Старт

Как устроен этот конспект

Перед тобой русскоязычный конспект курса The Context Course — курса Hugging Face про контекст-инжиниринг для кодовых агентов (Claude Code, Codex, OpenCode и других). Автор оригинала — Ben Burtenshaw с командой соавторов.

Важно про формат Это не официальный перевод, а авторский пересказ: у оригинального репозитория на момент написания не было открытой лицензии, поэтому дословно переводить его нельзя. Зато структура сохранена один в один — все 7 юнитов и все главы в исходном порядке, ни одна тема не выброшена. В конце каждой главы есть ссылка на английский оригинал: если что-то захочется перепроверить — оригинал в одном клике.

Что здесь интерактивного

  • Квизы — все квизы курса воссозданы в кликабельном виде: жмёшь на вариант, сразу видишь объяснение и счёт.
  • Табы агентов — инструкции для Claude Code / Codex / OpenCode / Pi переключаются табами, и выбор запоминается на всём сайте: выбрал своего агента один раз — везде видишь его команды.
  • Виджеты — вместо iframe-демок оригинала здесь свои интерактивные схемы: анатомия скилла, прогрессивная подгрузка, агентский цикл.
  • Прогресс — прочитанные главы отмечаются галочками в оглавлении (хранится локально в браузере).

Как читать

Порядок глав — ровно как в курсе: теория → практика → квиз. Кнопки «Дальше» внизу каждой главы ведут по этому маршруту. Если ты тут за чем-то конкретным — оглавление слева, каждая глава самодостаточна.

Юнит 0 · Онбординг

Знакомство с курсом

Курс целиком про одну дисциплину — контекст-инжиниринг для кодовых агентов: как структурировать знания так, чтобы агент сам находил нужное в нужный момент и выдавал результат заметно лучше.

Почему это ключевой навык

У Claude Code, Codex и OpenCode одно общее ограничение: агент ровно настолько хорош, насколько хорош его контекст. Хороший контекст — это меньше блужданий по тупикам, чище диффы и меньше переделок. Не «нужна модель поумнее», а «нужен контекст получше» — вот главный тезис курса.

За шесть юнитов ты научишься собирать переносимые скиллы, подключать инструменты через Model Context Protocol (MCP), упаковывать всё это в плагины, координировать сабагентов на крупных задачах — а в бонусном юните разберёшь, как минимальный агентский цикл устроен под капотом.

Программа

ЮнитТемаЧему научишься
0ОнбордингОбзор курса, установка инструментов, пререквизиты
1СкиллыЧто такое скиллы, как их писать, шарить и как агенты их подгружают
2MCPЧто такое MCP, подключение инструментов и API к агентам
3ПлагиныСборка плагинов, проектирование агентских воркфлоу
4СабагентыЗапуск специализированных агентов, мультиагентные паттерны
5ХукиНаблюдение, блокировка и автоматизация жизненного цикла агента
6Бонус: Nano HarnessМинимальный агентский цикл с нуля

Что нужно перед стартом

  • Базовый Python: переменные, функции, циклы, работа с файлами.
  • Уверенность в терминале: перейти в каталог, запустить скрипт.
  • Аккаунт на huggingface.co.
  • Хотя бы один установленный кодовый агент — см. ниже.

Установка инструментов

Курс мультиагентный: выбери минимум одного агента и проходи материал с ним.

Заметка Референсные агенты этого издания — Claude Code, Codex и OpenCode. Если ты на Cursor или GitHub Copilot — идеи те же, но их UX для MCP и расширений отличается, пошагово они в курсе не разбираются.

Официальный кодовый агент Anthropic: веб, десктоп-приложение или CLI.

curl -fsSL https://claude.ai/install.sh | bash

Старт: открой claude.ai/code в браузере или запусти claude в каталоге любого проекта — при первом запуске попросит залогиниться.

Кодовый агент OpenAI с мультиагентными возможностями.

npm install -g @openai/codex

Старт: запусти codex и выбери «Sign in with ChatGPT» (нужен платный план — Plus, Pro, Business, Edu или Enterprise) либо используй API-ключ OpenAI.

Опенсорсный агент от opencode.ai.

curl -fsSL https://opencode.ai/install | bash

Или через npm:

npm install -g opencode-ai

Старт: запусти opencode в каталоге проекта. Поддерживает разных LLM-провайдеров — своего выберешь при первом запуске.

Минималистичный терминальный харнес, расширяемый скиллами, шаблонами промптов и TypeScript-расширениями.

npm install -g @mariozechner/pi-coding-agent

Старт: запусти pi в каталоге проекта. Авторизация — через /login (подписочный провайдер) или экспортируй API-ключ вроде ANTHROPIC_API_KEY до запуска.

Как проходить курс

Темп. Рассчитывай на юнит в неделю, примерно 2–3 часа на каждый. Контекст-инжиниринг — навык из разряда «руками», так что примеры лучше собирать, а не пролистывать.

Формат. Каждый юнит — это теория + запускаемый код + практический проект + короткий квиз.

Свой маршрут. Рекомендованный порядок — подряд, но можно и по потребностям:

  • Нужны только скиллы? Начни с юнита 1, к MCP вернёшься по мере надобности.
  • Пилишь плагин для команды? Стартуй с юнита 3.
  • Интересуют мультиагентные системы? Иди в юнит 4, юниты 1–2 держи как справочник.
  • Сидишь на опенсорсе? Во всех уроках примеры для OpenCode идут наравне с Claude Code и Codex.

Сертификаты

У курса два уровня сертификации, оба сертификата показываются в профиле на Hugging Face:

СертификатУсловияСрок
Context FundamentalsКвизы юнитов 1–2 на 70%+2–3 недели
Context EngineeringВсе квизы юнитов 1–5 на 70%+ и финальный (capstone) проект5–8 недель
Совет Capstone-проект анонсируют в лайв-стриме курса. Чтобы не пропустить — подпишись на организацию Context Course на Hugging Face.

Структура юнитов

Каждый юнит устроен одинаково: введение → теория → практические разборы → hands-on-проект → квиз. Стартовые шаблоны и готовый к копипасту код прилагаются, чтобы время уходило на идеи, а не на бойлерплейт.

Авторы курса

  • Ben Burtenshaw — ML-инженер в Hugging Face; занимается LLM-приложениями, пост-трейнингом и агентными подходами, ведёт направление лучших практик для агентов и контекст-инжиниринга.
  • Atin Kumar Singh — ML-исследователь (world models и робототехника в DARE Lab, UC Davis), founding engineer в Data Pigeon.
  • Maya Nielan и Ryan Whitehead — соавторы разделов про Claude Code.

Комьюнити

  • Discord: discord.gg/huggingface
  • Делись результатами с тегом #ContextCourse
  • Ошибки в материалах курса — через GitHub Issues репозитория

Что дальше

Поставь хотя бы одного агента из списка выше, сверься с пререквизитами — и вперёд, в юнит 1, к скиллам.

Юнит 1 · Скиллы

Введение: зачем агентам скиллы

Скиллы — базовый строительный блок агентского контекста. В этом юните разберёмся, что это такое, как они устроены, как их ставить и писать самому.

Почему контекст-инжиниринг вообще важен

Без правильного контекста даже сильный агент работает вслепую: затыкает дыры догадками, задаёт вопросы, которых не должен задавать, и тратит время на работу, которую придётся переделывать. Он начинает валиться не только на сложных задачах, но и на рутинных. И решение тут — не «модель получше», а контекст получше.

Классический пример: попроси Claude Code «опубликуй датасет на Hugging Face» без какого-либо контекста — и агенту придётся гадать про аутентификацию, схему данных и обязательные поля. Он застрянет, начнёт переспрашивать или подставит правдоподобные, но неверные значения. Время и токены — в трубу.

Дай тому же агенту инструкции по аутентификации, схему датасета, формат API и список типичных граблей — и та же задача выполнится быстрее и надёжнее.

Главный тезис Кодовый агент хорош ровно настолько, насколько хорош его контекст. Контекст-инжиниринг — это практика структурирования контекста так, чтобы агент мог его найти и применить.

Эта мысль сквозная для всего курса. По той же причине сообщества публикуют общие контексты, а команды вкладываются в то, чтобы документация жила рядом с агентом.

Так что такое скилл?

Скилл — самодостаточный пакет знаний, который делает агента экспертом в одной конкретной задаче. Скиллы переносимы между проектами и агентами, переиспользуются после установки, имеют структуру, которую агент умеет парсить, и расширяются ссылками на скрипты и API.

По сути скилл — это онбординг-документ для агента: та же пошаговая инструкция, которую ты дал бы новому коллеге, только написанная так, чтобы агент мог подгрузить её по запросу.

Физически скилл — это обычно каталог с markdown-файлом SKILL.md (метаданные + инструкции), рядом с которым лежат скрипты и справочные материалы — набор зависит от типа скилла.

От промптов к переносимым знаниям

Ключевое отличие скилла от длинного промпта: скилл структурирован, переиспользуем и обнаружим. Он живёт дольше одного диалога и подгружается только тогда, когда релевантен. В следующей главе копнём глубже.

Что будет в юните 1

К концу юнита ты будешь понимать: что такое скиллы и зачем они нужны; что такое Agent Skills Specification — открытый стандарт структуры скиллов; формат SKILL.md; как использовать скиллы с агентами; как собрать свой первый скилл с нуля и как его дебажить, когда он не срабатывает.

Спецификация Agent Skills

У скиллов есть формальный стандарт — Agent Skills Specification, он живёт на agentskills.io.

Факт Спецификация задаёт переносимый формат упаковки агентских знаний. Изначально её создала Anthropic, сейчас стандарт поддерживают 30+ кодовых агентов по всему миру.

Что гарантирует спека:

  • скиллы работают в разных агентах и на разных платформах;
  • агенты могут автоматически находить и подгружать релевантные скиллы;
  • сообщества могут вместе шарить и улучшать скиллы;
  • вокруг скиллов можно строить тулинг и платформы.

Почему скиллы — это важно

Без скиллов контекст-инжиниринг выглядит так: длинные повторяющиеся промпты в каждом диалоге, разрозненные вики-страницы, и каждый в команде собирает один и тот же контекст заново. Со скиллами контекст определяется один раз, обновляется в одном месте, комбинируется между доменами и подгружается агентом по мере надобности.

Пререквизиты юнита

  • Настроенный кодовый агент (Claude Code, Codex или опенсорсный).
  • Python для вспомогательных скриптов.
  • Аккаунт Hugging Face.
  • Базовые навыки работы в терминале.

Если что-то не готово — вернись в юнит 0, раздел про установку.

Ключевое Скиллы — это переносимый, структурированный контекст, который делает агентов лучше в конкретных задачах. Они следуют Agent Skills Specification для кросс-агентной совместимости и сидят в самом сердце контекст-инжиниринга.

Юнит 1 · Скиллы

Что такое скиллы (подробно)

Разбираем скиллы глубже: какие проблемы они решают и чем отличаются от промптов.

Та же проблема, но воркфлоу длиннее

Во введении был простой пример «опубликуй датасет». Теперь поднимем ставки:

«Обучи модель на этом датасете и опубликуй её, когда закончишь».

Аргумент тот же, только с эскалацией: недостающий контекст теперь накапливается по цепочке связанных решений, а не в одной точке. Без нормального контекста агент, скорее всего, споткнётся трижды:

Фейл 1: аутентификация.

Error: Unable to authenticate with Hugging Face Hub
Hint: Set your HF_TOKEN environment variable

Агент знает, что нужна аутентификация, но не знает, как она настроена именно в твоём окружении.

Фейл 2: неправильная конфигурация.

ValueError: Dataset format not recognized
Expected: parquet, csv, or arrow
Got: .npy files

Агент не в курсе формата твоего датасета и того, как его конвертировать.

Фейл 3: пропущенные бест-практики.

Model uploaded successfully!

Модель залита — но без README, без карточки модели с деталями обучения, без лицензии и без примеров использования.

Все три фейла — от нехватки доменных знаний. Писать код агент умеет, а вот специфику твоего воркфлоу, инструментов и практик — не знает.

Скилл как решение

Скилл упаковывает доменные знания в структурированный переиспользуемый формат, который агент находит и применяет сам. Внутри может быть:

  • метаданные, по которым агент решает, когда скилл применить;
  • пошаговые инструкции;
  • вспомогательные скрипты для рутинных операций;
  • ссылки на документацию и примеры кода;
  • гайд по типичным проблемам.

С условным скиллом «публикация датасетов» тот же агент: сам обнаружит скилл, как только речь зайдёт о публикации → подгрузит инструкции по аутентификации и скрипты валидации → применит бест-практики без напоминаний → доведёт задачу до конца.

Промпт-подход и его потолок

Без скиллов ту же проблему решают «мегапромптом»: «Ты — эксперт по Hugging Face. При публикации моделей сначала аутентифицируйся через HF_TOKEN, потом проверь, что датасет в CSV с колонками text/label/split, обучай с такими-то гиперпараметрами, после обучения собери карточку модели с метриками и лицензией…» — и так на сотни строк.

Проблемы такого подхода:

  • Не переиспользуется — вставляешь в каждый диалог заново.
  • Не шарится — коллеги копипастят каждый сам себе.
  • Не поддерживается — обновил в одном месте, обновляй везде.
  • Не обнаруживается — как другой команде вообще узнать про эти знания?
  • Не комбинируется — несколько доменов в одном промпте смешиваются плохо.
  • Не версионируется — ни истории изменений, ни отката.

Скилл-подход: структура и переносимость

Скилл упаковывает те же знания в стандартную структуру. Если коротко, стандарт описывает четыре вещи:

  1. Файловую структуру: каталог skill-name/ с SKILL.md, scripts/, references/.
  2. Формат метаданных: YAML-фронтматтер с name, description и т.д.
  3. Протокол обнаружения: как агенты находят и подгружают скиллы.
  4. Гарантии совместимости: что должен уметь агент, чтобы поддерживать скиллы.

Думай об этом как о package.json для скиллов: совместимость + возможность строить тулинг.

Минимальная структура:

skill-name/
└── SKILL.md              # обязателен: метаданные + инструкции

Внутри SKILL.md — фронтматтер и инструкции:

---
name: "huggingface-model-publishing"
description: "Публикация моделей на Hugging Face Hub. Использовать при
  загрузке моделей, создании карточек моделей и управлении версиями."
---
# Скилл публикации моделей на Hugging Face

...

Что получаем взамен промпта: переиспользование между проектами и командами, шаринг через репозитории и version control, поддержку в одном месте, комбинируемость с другими скиллами, автоматическую подгрузку совместимыми агентами и открытый стандарт под всем этим.

Progressive disclosure: как агент подгружает скиллы

Агент не тащит все скиллы в контекст при старте — это было бы расточительно. Работает «прогрессивная подгрузка»: сначала только метаданные, полный текст — когда задача совпала. Пощёлкай по шагам:

Интерактив · прогрессивная подгрузка скилла

dataset-publishing · ~30 токенов
model-training · ~30 токенов
gradio-ui · ~30 токенов
1/4

Всё это происходит автоматически и прозрачно — руками активировать скиллы не нужно, агент находит и подгружает их сам, отталкиваясь от задачи.

Примеры скиллов из реальной жизни

Вот что лежит в репозитории скиллов Hugging Face:

СкиллЧто делает
Scientific Paper ReviewПоиск научных статей
model-trainingОбучение моделей через TRL и популярные фреймворки
gradio-ui-builderСборка веб-интерфейсов для моделей

Каждый такой скилл — это доменная экспертиза, на которую по документации ушли бы часы, а агент получает её мгновенно.

Дальше

В следующей главе — сам формат SKILL.md.

Юнит 1 · Скиллы

Формат SKILL.md

По спецификации скилл — это файл SKILL.md с YAML-фронтматтером и markdown-инструкциями, опционально дополненный каталогами scripts/, references/ и assets/. Пройдёмся по каждой части и по тому, как писать их хорошо.

Интерактив · анатомия скилла (кликни по файлу)

  • 📁 skill-name/
    • 📁 scripts/
    • 📁 references/
    • 📁 assets/
Выбери файл слева — здесь появится описание его роли.

Структура каталога

skill-name/
├── SKILL.md              # обязателен: метаданные + инструкции
├── scripts/              # опционально: исполняемый код
│   ├── validate.py
│   ├── deploy.sh
│   └── README.md
├── references/           # опционально: справочные материалы
│   ├── api-reference.md
│   └── examples.md
└── assets/               # опционально: шаблоны, данные
    ├── template.yaml
    └── config-example.json

Правила:

  • имя каталога — строчными буквами через дефис (skill-name, не SkillName);
  • двойные дефисы запрещены (my--skill — невалидно);
  • SKILL.md обязателен и лежит в корне;
  • scripts/, references/, assets/ — по желанию;
  • все относительные пути считаются от корня скилла.

Поля фронтматтера

Каждый SKILL.md начинается с YAML-фронтматтера. Полный вариант выглядит так:

---
name: "dataset-publishing"
description: "Публикация датасетов на Hugging Face Hub. Использовать при
  загрузке датасетов, создании карточек и управлении версиями."
license: "Apache-2.0"
compatibility: "Проверено на Python 3.8+ и huggingface_hub 0.16+"
metadata:
  author: "ml-team"
  version: "1.0.0"
allowed-tools: "Bash(hf:*) Python(huggingface_hub:*)"
---

Обязательные поля

name (строка) — человекочитаемый идентификатор. Ограничения: строчные буквы, цифры и дефисы; 1–64 символа; без двойных дефисов; должен точно совпадать с именем каталога. Примеры: text-classification, model-evaluation, dataset-publishing.

description (строка) — краткое описание, максимум 1024 символа. Должно отвечать на два вопроса: что скилл делает и когда его применять. Начинай с глагола: «Публикует датасеты…», «Обучает модели…».

Опциональные поля

  • license — лицензия скилла: MIT, Apache-2.0, CC-BY-4.0, GPL-3.0. Говорит другим, как можно использовать и модифицировать скилл.
  • compatibility — требования к окружению, до 500 символов: «Проверено на Python 3.8+ и huggingface_hub 0.16+», «Нужен PyTorch 2.0+, transformers 4.30+ и CUDA 11.8», «Работает с Claude Code, Codex и опенсорсными агентами».
  • metadata — произвольные ключ-значение. Типичные ключи: author, version, created, updated, maintainer, contact.
  • allowed-tools (экспериментальное) — glob-паттерны инструментов, которыми скилл пользуется. Формат: ToolName(pattern:*) через пробел, например "Bash(git:*) Bash(jq:*) Read". Агенты могут ограничивать выполнение скилла этими инструментами; поддерживают поле пока не все.

Тело скилла

После фронтматтера идут подробные инструкции в markdown. Типовой скелет:

# Скилл публикации датасетов

## Обзор
Коротко: чему учит скилл и типовые сценарии.

## Чек-лист пререквизитов
- [ ] Python 3.8+
- [ ] Установлен huggingface_hub
- [ ] Аккаунт Hugging Face
- [ ] Задан HF_TOKEN

## Пошаговый гайд

### 1. Аутентификация
...

Гайдлайны по содержимому. Держи SKILL.md сфокусированным: 400–800 строк максимум; развёрнутую справку выноси в references/, примеры кода — в scripts/; на другие файлы ссылайся внутренними ссылками вида [скрипт валидации](scripts/validate.py).

Пиши ясно: используй ## Разделы и ### Подразделы, давай готовый к копипасту код в fenced-блоках, нумеруй шаги процедур, оформляй пререквизиты чек-листами. На внешние ресурсы — ссылки на документацию, API-гайды и примеры.

Ссылки на файлы внутри скилла

На скрипты и ассеты ссылайся относительными путями от корня скилла:

## Валидация

Запусти скрипт валидации:
```bash
python scripts/validate_dataset.py data/my_dataset.csv
```

Вспомогательные скрипты

Скрипты живут в scripts/ и могут быть на любом языке. Например, scripts/validate_dataset.py:

import sys
from pathlib import Path

import pandas as pd

REQUIRED = {"text", "label"}

def validate(path: str) -> None:
    if not Path(path).exists():
        raise FileNotFoundError(f"Файл не найден: {path}")

    df = pd.read_csv(path)

    missing = REQUIRED - set(df.columns)
    if missing:
        raise ValueError(f"Не хватает колонок: {missing}")

    if df.isnull().any().any():
        raise ValueError("В датасете есть пропуски")

if __name__ == "__main__":
    try:
        validate(sys.argv[1])
        print("Валидация пройдена!")
    except Exception as e:
        print(f"Валидация провалена: {e}")
        sys.exit(1)
Совет Не перегружай скрипты параметрами. В большинстве случаев агент сам перегенерирует скрипт с нужными параметрами — достаточно заложить ключевые переменные, чтобы сфокусировать его на основной функциональности.

Каталог references/

Сюда складывается документация, на которую ссылается SKILL.md: например, api-reference.md с выжимкой из API Hugging Face Hub или examples.md с описаниями типовых датасетов (имя, формат, колонки, размер).

Каталог assets/

Шаблоны и конфиги. Например, заготовка карточки датасета dataset-card-template.md с плейсхолдерами {DATASET_NAME} и {Brief description}, или config-example.yaml:

dataset:
  name: my-dataset
  version: 1.0
  format: csv
Полный пример маленького, но законченного скилла

model-card-generator/SKILL.md — скилл генерации карточек моделей:

---
name: "model-card-generator"
description: "Генерация карточек моделей для Hugging Face Hub.
  Использовать при документировании моделей, добавлении метаданных
  и создании README."
license: "MIT"
compatibility: "Python 3.8+, нужен jinja2"
metadata:
  author: "context-course"
  version: "1.0.0"
---

# Скилл генерации карточек моделей

## Обзор
Помогает создавать профессиональные карточки моделей и README
для публикации на Hugging Face Hub. Карточка документирует:
- архитектуру и обучающие данные
- назначение и ограничения
- метрики качества
- этические соображения
- лицензию

## Чек-лист пререквизитов
- [ ] Python 3.8+
- [ ] Установлен jinja2
- [ ] Известны имя, описание и метрики модели

## Пошаговый гайд

### 1. Собери информацию о модели
Имя и описание, описание обучающих данных, метрики
(accuracy, F1, BLEU...), сценарии применения, ограничения,
лицензия (MIT, Apache-2.0, CC-BY-4.0...).

### 2. Сгенерируй карточку
```python
from generate_card import create_model_card

card = create_model_card(
    model_name="My Classifier",
    description="Текстовый классификатор на трансформере",
    metrics={"accuracy": 0.92, "f1_score": 0.89},
)
with open("README.md", "w") as f:
    f.write(card)
```
Подробнее — в [скрипте генерации](scripts/generate_card.py).

### 3. Доработай руками
Добавь в README.md: развёрнутое описание, процедуру обучения,
пример использования, информацию для цитирования.

## Скрипты
- [Генератор карточек](scripts/generate_card.py)
- [Валидатор](scripts/validate_card.py)

## Справка
- [Статья про Model Cards](https://arxiv.org/abs/1810.03993)

## Траблшутинг

### Фронтматтер не рендерится
Проверь, что дефисы стоят на отдельных строках YAML-секции.

### Символы отображаются криво
Сохрани файл в UTF-8 (проверить: `file README.md`).

scripts/generate_card.py — Jinja2-шаблон карточки + функция create_model_card(), которая рендерит имя, описание, таблицу метрик, дату создания, сценарии применения и ограничения.

references/best-practices.md — памятка: описывай модель одним предложением, указывай источник данных и архитектуру, приводи реальные метрики (а не голословные заявления) на стандартных бенчмарках, честно документируй ограничения; лицензию выбирай пермиссивную (MIT/Apache-2.0), CC-BY-4.0 — для не-софтового контента, CC0 — для датасетов, и не забывай про лицензии зависимостей.

Валидация через skills-ref

В спецификацию входит инструмент валидации skills-refagentskills.io):

# Проверить каталог скилла
skills-ref validate skill-name/

# Вывод:
# ✓ SKILL.md найден
# ✓ поле name валидно: model-card-generator
# ✓ description валиден (до 1024 символов)
# ✓ обязательные поля на месте
# ⚠ warning: скрипты используют абсолютные пути (нужны относительные)

Размеры: сколько вешать в строках

КомпонентРекомендацияЗачем
SKILL.md400–800 строкЛёгкое обнаружение и загрузка
scripts/каждый скрипт < 500 строкЛегко понять и поправить
Скилл целиком< 2 МББыстрое скачивание и индексация

Тяжёлые справочные материалы выноси во внешние репозитории: сам скилл остаётся маленьким и лёгким, а увесистая справка подтягивается извне только при необходимости.

Советы по написанию

Совет Пиши инструкции для того, кто задачу не знает. Считай, что агенту (и читателю) нужны ясные пошаговые указания.
Хорошо«Сначала аутентифицируйся командой hf auth login — она сохранит токен локально для последующих команд».
Плохо«Аутентифицируйся в HF».
Совет Код — только готовый к копипасту. Прогони его сам, прежде чем класть в скилл.
Хорошоpip install huggingface_hub
hf auth login
Плохо«Установи huggingface_hub и залогинься».
Внимание Описания должны быть реалистичными и конкретными. Размытый description не поможет ни агенту, ни людям найти твой скилл.
Хорошо«Публикация датасетов на Hugging Face Hub. Использовать при загрузке датасетов, создании карточек и управлении версиями».
Плохо«Скилл про датасеты».

Дальше

Следующий шаг — как разные агенты подгружают скиллы, а потом соберём свой с нуля.

Юнит 1 · Квиз

Квиз 1: скиллы и спецификация

Проверь, как усвоились скиллы и Agent Skills Specification, прежде чем идти дальше. Жми на вариант — объяснение появится сразу.

Что такое скилл?

Что определяет Agent Skills Specification?

Какие поля фронтматтера в SKILL.md обязательны?

Как агенты обнаруживают и подгружают скиллы?

Сколько агентов поддерживают Agent Skills Specification?

Как оценить себя

  • 5/5 — отлично, у тебя цельное понимание скиллов и спецификации, двигайся дальше.
  • 4/5 — хорошо; глянь ещё раз темы промахнувшихся вопросов.
  • 3/5 и меньше — стоит перечитать главы «Что такое скиллы» и «Формат SKILL.md».

Готов узнать, как скиллы использовать с разными агентами? Следующая глава — про это.

Юнит 1 · Скиллы

Использование скиллов с агентами

Формат SKILL.md благодаря спецификации везде одинаков. Различается другое: где агент ищет скиллы на диске и как их ставить.

Где живут скиллы

Каждый агент сканирует свои каталоги. Скилл создаётся просто: кладёшь SKILL.md в именованный подкаталог по одному из путей:

СкоупПутьДействует на
Личный~/.claude/skills/<name>/SKILL.mdВсе твои проекты
Проектный.claude/skills/<name>/SKILL.mdТолько этот проект

Claude Code следит за изменениями: добавил, поправил или удалил скилл — изменения подхватываются прямо в текущей сессии, без перезапуска.

СкоупПуть
Репозиторий.agents/skills/<name>/SKILL.md
Пользователь~/.agents/skills/<name>/SKILL.md
Админский/etc/codex/skills/<name>/SKILL.md

Codex смотрит во все три места; если имя скилла совпадает — приоритет у репозиторного, затем пользовательский, затем админский.

Проектные пути:

  • .opencode/skills/<name>/SKILL.md
  • .claude/skills/<name>/SKILL.md
  • .agents/skills/<name>/SKILL.md

Глобальные пути:

  • ~/.config/opencode/skills/<name>/SKILL.md
  • ~/.claude/skills/<name>/SKILL.md
  • ~/.agents/skills/<name>/SKILL.md

OpenCode идёт вверх от текущего каталога до корня git-репозитория, проверяя каждый уровень.

СкоупПуть
Проект.pi/skills/<name>/SKILL.md
Общее дерево репо.agents/skills/<name>/SKILL.md
Пользователь~/.pi/agent/skills/<name>/SKILL.md
Общее пользовательское~/.agents/skills/<name>/SKILL.md

Pi сканирует и свои пути, и общее дерево .agents/skills/ — один и тот же скилл работает в Pi и других харнесах без дублирования.

Установка скилла

Разберём на живом примере — скилле hf-cli: он даёт агенту доступ к CLI Hugging Face Hub для скачивания, загрузки и управления репозиториями.

У Hugging Face CLI есть кросс-агентный установщик скиллов — удобно, когда один скилл нужен в нескольких агентах или хочется единый флоу установки:

# Установка в проект
hf skills add

# Симлинки под конкретных агентов
hf skills add --claude
hf skills add --codex --opencode --global

Скиллы распространяются как плагины через маркетплейсы. Внутри сессии Claude Code добавь маркетплейс и поставь плагин со скиллом:

/plugin marketplace add huggingface/skills
/plugin install hf-cli@huggingface-skills

Команда /plugin — просмотр, включение и выключение установленных плагинов. Скиллы из плагинов неймспейсятся как /<плагин>:<скилл> (например, /hf-cli:download-model), чтобы не конфликтовать.

Встроенный установщик для курируемых скиллов:

$skill-installer hf-cli

Либо поставь весь плагин Hugging Face: набери /plugins, выбери «Hugging Face» и нажми «Install plugin».

Создай каталог скилла в любом поддерживаемом месте и положи туда SKILL.md:

hf skills add

Команда создаст .agents/skills/hf-cli/SKILL.md — OpenCode найдёт его сам.

Какими скиллами агенту можно пользоваться, контролируется через opencode.json:

{
  "permission": {
    "skill": {
      "*": "allow",
      "experimental-*": "ask"
    }
  }
}

Pi видит общее дерево .agents/skills/, которое создаёт hf skills add, так что самый простой флоу:

# Создаст .agents/skills/hf-cli/SKILL.md
hf skills add

# Запусти Pi в том же репозитории
pi

Для разового теста скилл можно подгрузить явно: pi --skill /абсолютный/путь/до/SKILL.md.

Вызов скиллов

Установленный скилл активируется двумя способами: неявно (агент сматчил твой запрос с description скилла) или явно (ты вызвал скилл по имени).

# Неявно — Claude матчит запрос с описанием скилла
Скачай последнюю версию meta-llama/Llama-4-Scout-17B-16E

# Явно — вызов скилла из плагина по неймспейсному имени
/hf-cli:download-model
# Неявно — Codex выбирает по совпадению с задачей
codex "Залей мою модель на Hub"

# Явно — упомяни по имени
$hf-cli

Агент вызывает скиллы через нативный инструмент skill, передавая имя. Матчинг задач со скиллами OpenCode делает автоматически по description из фронтматтера.

# Неявно — Pi выбирает по совпадению с задачей
Залей мою модель на Hub

# Явно — форсируем скилл
/skill:hf-cli Залей мою модель на Hub

Траблшутинг

Скилл не активируется

Проблема: скилл установлен, но агент им не пользуется.

Проверь, что в description есть ключевые слова из твоих запросов; формулируй запросы в терминах назначения скилла; либо вызывай скилл явно по имени. Описание вида «CLI Hugging Face Hub для скачивания, загрузки и управления репозиториями» сработает на запросах со словами «upload», «download», «Hugging Face» — а размытое «закинь мои файлы куда-нибудь» может и не сматчиться.

Сработал ли скилл? Быстрый тест на активацию

Самый быстрый способ отладить новый скилл — проверить «давление активации» напрямую:

  1. Попроси о задаче очевидным промптом, который обязан сматчиться с description.
  2. Повтори более размытым промптом — таким, как написал бы реальный пользователь.
  3. Если срабатывает только первый — ужесточай description, пока оба промпта не начнут стабильно активировать скилл.

В Codex именно здесь помогает $skill-creator: скорми ему скилл, несработавший промпт и желаемое поведение — он подточит description, не переписывая скилл целиком.

Скилл не найден

Проблема: агент не может найти скилл. Проверь, что он лежит в правильном каталоге для твоего агента:

ls .claude/skills/hf-cli/SKILL.md

Или проверь установленные плагины внутри сессии командой /plugin.

ls .agents/skills/hf-cli/SKILL.md
ls .opencode/skills/hf-cli/SKILL.md
ls .pi/skills/hf-cli/SKILL.md
ls .agents/skills/hf-cli/SKILL.md

Pi читает оба пути, плюс пользовательские каталоги ~/.pi/agent/skills/ и ~/.agents/skills/.

Конфликты скиллов

Проблема: функциональность нескольких скиллов пересекается. Делай description каждого скилла достаточно конкретным, чтобы агент их различал. Если два скилла закрывают смежные задачи (например, валидация и публикация) — очерти каждому свой скоуп прямо в описании.

Бест-практики

Группируй скиллы по домену — родственные скиллы рядом легче обнаруживаются:

.claude/skills/
├── hf-cli/
│   └── SKILL.md
├── dataset-validation/
│   └── SKILL.md
├── model-training/
│   └── SKILL.md
└── model-evaluation/
    └── SKILL.md

Держи каждый скилл сфокусированным на одной задаче. Если SKILL.md перевалил за 500 строк — распили на несколько скиллов или вынеси справку в соседние файлы.

Дальше

Пора собрать скилл с нуля.

Юнит 1 · Практика

Собираем первый скилл

В этой практике ты соберёшь с нуля скилл валидации датасетов Hugging Face: проверка формата и схемы, отлов типовых проблем с качеством данных и короткий отчёт. На выходе — рабочий скилл по спецификации, который крутится локально с твоим агентом.

Шаг 1. Каталог скилла

# Создаём каталог скилла
mkdir hf-dataset-validation
cd hf-dataset-validation

# Структура по спецификации
mkdir -p scripts references assets

# Основные файлы
touch SKILL.md .gitignore requirements.txt

Получается такая структура:

hf-dataset-validation/
├── SKILL.md              # главный файл скилла (обязателен)
├── requirements.txt      # Python-зависимости
├── .gitignore            # правила игнора для git
├── scripts/              # опционально: скрипты
│   ├── validate_dataset.py
│   └── generate_report.py
├── references/           # опционально: документация
│   └── examples.md
└── assets/               # опционально: шаблоны
    └── validation-template.txt

Шаг 2. Пишем SKILL.md

Главный файл — фронтматтер плюс инструкции. Скелет содержимого:

---
name: "hf-dataset-validation"
description: "Валидация датасетов Hugging Face: схема, формат, качество
  данных. Использовать при проверке датасетов перед публикацией,
  обучением или шарингом."
license: "MIT"
compatibility: "Python 3.8+, нужны pandas и datasets"
metadata:
  author: "your-username"
  version: "1.0.0"
---

# Скилл валидации датасетов

## Обзор
Скилл учит агента валидировать датасеты Hugging Face:
- Схема: правильные колонки и типы данных
- Качество: пропуски, дубликаты, выбросы
- Формат: CSV, Parquet, Arrow, JSON
- Размеры: вес файлов, число записей, потребление памяти

Применять перед публикацией датасетов или обучением на них.

Дальше в теле — чек-лист пререквизитов (Python 3.8+, pandas, datasets, доступный локально файл датасета) и пошаговый гайд. Ключевые шаги гайда:

Установка зависимостей:

pip install pandas datasets numpy

Определение формата — по расширению файла (.csv / .parquet / .json).

Загрузка и осмотр:

import pandas as pd

df = pd.read_csv("data/my_dataset.csv")

print(f"Размерность: {df.shape}")   # (строки, колонки)
print(f"Колонки: {list(df.columns)}")
print(f"Типы:\n{df.dtypes}")

Валидация схемы — сверяем ожидаемые и фактические колонки:

expected = {"text", "label", "split"}
actual = set(df.columns)

missing = expected - actual
if missing:
    print(f"ОШИБКА: не хватает колонок: {missing}")

extra = actual - expected
if extra:
    print(f"ПРЕДУПРЕЖДЕНИЕ: лишние колонки: {extra}")

if df["label"].dtype not in ("int64", "object"):
    print("ПРЕДУПРЕЖДЕНИЕ: label должен быть целым числом или строкой")

Проверка качества данных — пропуски, дубликаты, пустые строки:

missing_count = df.isna().sum()
if missing_count.any():
    print("Найдены пропуски:")
    print(missing_count[missing_count > 0])

duplicates = df.duplicated().sum()
if duplicates > 0:
    print(f"ПРЕДУПРЕЖДЕНИЕ: дубликатов строк: {duplicates}")

for col in df.select_dtypes(include="object").columns:
    empty = (df[col].str.strip() == "").sum()
    if empty > 0:
        print(f"ПРЕДУПРЕЖДЕНИЕ: в колонке '{col}' пустых строк: {empty}")

Генерация отчёта — вызовом готового скрипта:

python scripts/validate_dataset.py data/my_dataset.csv

В отчёте: сводка по датасету (строки, колонки, размер), метрики качества, найденные проблемы и рекомендации по исправлению.

Плюс раздел «Типичные проблемы и решения»:

  • UnicodeDecodeError при чтении CSV — перебери кодировки (utf-8, latin-1, iso-8859-1) в try/except, пока файл не откроется.
  • MemoryError на больших файлах — читай чанками: pd.read_csv("large.csv", chunksize=10000) и обрабатывай по кускам.
  • Разнобой в именах колонок (регистр, пробелы) — нормализуй: df.columns = df.columns.str.lower().str.replace(" ", "_").

И два скрипта в scripts/:

scripts/validate_dataset.py — машиночитаемая валидация с JSON-отчётом:

#!/usr/bin/env python3
"""Валидация датасетов Hugging Face."""

import json
import sys
from pathlib import Path

import pandas as pd

def validate_csv(filepath):
    """Проверить CSV-файл датасета."""
    errors, warnings = [], []
    report = {
        "filepath": filepath,
        "format": "csv",
        "errors": errors,
        "warnings": warnings,
        "metadata": {},
    }

    if not Path(filepath).exists():
        errors.append(f"Файл не найден: {filepath}")
        return report

    try:
        df = pd.read_csv(filepath)
        report["metadata"]["rows"] = len(df)
        report["metadata"]["columns"] = list(df.columns)
        report["metadata"]["dtypes"] = {k: str(v) for k, v in df.dtypes.items()}

        missing = df.isna().sum()
        if missing.any():
            report["metadata"]["missing_values"] = missing.to_dict()

        dup_count = df.duplicated().sum()
        if dup_count > 0:
            warnings.append(f"Дубликатов строк: {dup_count}")

        for col in df.select_dtypes(include="object").columns:
            empty = (df[col].str.strip() == "").sum()
            if empty > 0:
                warnings.append(f"В колонке '{col}' пустых строк: {empty}")

    except Exception as e:
        errors.append(f"Ошибка чтения файла: {e}")

    return report

def main():
    if len(sys.argv) < 2:
        print("Использование: python validate_dataset.py <файл>")
        sys.exit(1)

    report = validate_csv(sys.argv[1])
    print(json.dumps(report, indent=2, ensure_ascii=False))

    if report["errors"]:
        sys.exit(1)

if __name__ == "__main__":
    main()

scripts/generate_report.py — человекочитаемый отчёт: базовая статистика (строки, колонки, память через df.memory_usage(deep=True)), информация по каждой колонке (тип, количество непустых значений), качество данных (пропуски, дубликаты) и рекомендации — обработать пропуски, убрать дубликаты, задокументировать препроцессинг, добавить LICENSE и README.

Шаг 3. Документация

В references/examples.md — примеры использования: запуск валидации из терминала с образцом JSON-вывода (errors, warnings, metadata) и вызов validate_csv() прямо из Python.

Шаг 4. requirements.txt

pandas>=1.3.0
datasets>=2.0.0
numpy>=1.20.0

Шаг 5. Тестируем скилл

Прежде чем полагаться на скилл в реальной работе — прогони его:

# Тестовые данные
mkdir -p test_data
cat > test_data/sample.csv << 'EOF'
text,label,split
Hello world,0,train
Great job,1,train
This is bad,0,test
EOF

# Скрипт валидации
python scripts/validate_dataset.py test_data/sample.csv

# Генерация отчёта
python scripts/generate_report.py test_data/sample.csv

В выводе должны быть: число строк и колонок, типы данных, пропуски и дубликаты (если есть), рекомендации по качеству.

Шаг 6. Гигиена версионирования (опционально)

Это не специфично для скиллов, но стоит того, если планируешь итерировать или шарить:

git init
git add .
git commit -m "Initial dataset validation skill"

Относись к каталогу скилла как к обычному маленькому софтверному проекту: .gitignore, LICENSE перед публикацией, version control — чтобы правки инструкций было легко ревьюить.

Шаг 7. Тест с агентом

Для локальной итерации симлинкни скилл в каталог скиллов агента — правки будут подхватываться сразу. Копирование тоже работает, но с симлинками итерироваться посреди сессии сильно проще.

Заметка Команды ниже используют Unix-симлинки (ln -s). На Windows либо копируй каталог, либо создай симлинк каталога в PowerShell: New-Item -ItemType SymbolicLink -Path <куда> -Target <откуда>.
# Тестовый проект
mkdir my_project
cd my_project

# Симлинк скилла в проектный каталог скиллов
mkdir -p .claude/skills
ln -s /абсолютный/путь/до/hf-dataset-validation .claude/skills/hf-dataset-validation

# Запускаем Claude Code — скиллы он находит сам
claude

# В промпте:
# «Провалидируй мой датасет test_data/sample.csv»
# Тестовый проект
mkdir my_project
cd my_project

# Симлинк в репозиторный каталог скиллов
mkdir -p .agents/skills
ln -s /абсолютный/путь/до/hf-dataset-validation .agents/skills/hf-dataset-validation

# Запускаем Codex
codex

# В промпте:
# «Провалидируй мой датасет test_data/sample.csv»
# Тестовый проект
mkdir my_project
cd my_project

# Симлинк в проектный каталог скиллов
mkdir -p .opencode/skills
ln -s /абсолютный/путь/до/hf-dataset-validation .opencode/skills/hf-dataset-validation

# Запускаем OpenCode
opencode

# В промпте:
# «Провалидируй мой датасет test_data/sample.csv»
# Тестовый проект
mkdir my_project
cd my_project

# Симлинк в проектный каталог скиллов Pi
mkdir -p .pi/skills
ln -s /абсолютный/путь/до/hf-dataset-validation .pi/skills/hf-dataset-validation

# Запускаем Pi
pi

# В промпте:
# «Провалидируй мой датасет test_data/sample.csv»

Агент должен: найти скилл в локальном каталоге → сматчить задачу с description → подгрузить инструкции из SKILL.md в контекст → по мере надобности запускать скрипты.

Шаг 8. Отладка активации и доводка description

Теперь проверь, что скилл срабатывает стабильно:

Промпт 1: «Провалидируй датасет test_data/sample.csv перед обучением».
Промпт 2: «Глянь, этот CSV нормальный, можно его шарить?»

Если первый промпт срабатывает, а второй нет — твой description всё ещё слишком узкий. Дотачивай, пока оба не активируют скилл.

Тут снова полезен $skill-creator в Codex: дай ему скилл и промахнувшийся промпт — он перепишет только триггерное описание. В Claude Code и OpenCode тот же цикл делается руками: правишь фронтматтер → тестируешь в том же проекте → повторяешь.

Дальше

У тебя есть рабочий скилл. Продолжай пользоваться им и подкручивать description и скрипты — так скилл остаётся острым.

Юнит 1 · Квиз

Квиз 2: сборка и использование скиллов

Проверяем, как ты понял сборку, тестирование и использование скиллов по спецификации. Пять вопросов.

Ты собираешь скилл валидации датасетов. Какой должна быть структура каталога по спецификации?

Какие поля фронтматтера SKILL.md — единственные обязательные?

Скилл собран локально. Какой следующий шаг для его проверки — лучший?

Как делить контент между инструкциями SKILL.md и скриптами?

Скилл активируется нестабильно. Что делать в первую очередь?

Как оценить себя

  • 5/5 — отлично, ты понимаешь разработку скиллов и спецификацию, можно собирать боевые скиллы.
  • 4/5 — хорошо; освежи тему промахнувшегося вопроса.
  • 3/5 и меньше — перечитай главы про сборку и использование.

Итоги юнита 1

Ты прошёл юнит «Скиллы»! Теперь ты умеешь: собирать скиллы по Agent Skills Specification, использовать их с Claude Code, Codex и другими агентами, валидировать скилл в реальном проекте, писать ясные инструкции и вспомогательные скрипты, и дебажить активацию, когда скилл не срабатывает.

Дальше — юнит 2: Model Context Protocol (MCP): как подключать к агентам внешние инструменты и API. Погнали?

Юнит 2 · MCP

Введение в Model Context Protocol

Model Context Protocol (MCP) — открытый стандарт, который позволяет AI-приложениям взаимодействовать с внешними данными и инструментами. Он сидит прослойкой между твоим агентом и всем остальным миром: файлами, базами, API, сервисами.

В юните 1 были скиллы — статический, заранее написанный контекст, который живёт рядом с проектом. Для фиксированных знаний скиллы отличны, но они не умеют доставать свежую информацию и совершать живые действия. Эту дыру закрывает MCP: он даёт агенту динамический контекст — возможность вызывать функции, читать файлы, ходить в базы данных и API через один общий протокол.

Проблема M×N

Представь, что твой агент должен уметь: читать файлы с локальной машины, ходить в корпоративную базу, дёргать Slack API, доставать тикеты из Jira и искать по базе знаний.

Без стандартного протокола пришлось бы писать кастомную интеграцию под каждую пару «агент + источник данных». Это и есть проблема M×N: N агентов × M источников = N×M кастомных интеграций.

MCP убирает эту комбинаторику: источники данных оформляются как MCP-серверы один раз — и работают с любым MCP-совместимым агентом. Один адаптер — бесконечное переиспользование.

MCP как универсальный переходник

С точки зрения обучения MCP даёт одну согласованную схему, как выставлять агентам инструменты, ресурсы и промпты. Главная мысль этого введения проста: собери интеграцию один раз — переиспользуй её во всех совместимых агентах.

Точную терминологию — хост, клиент, сервер, типы возможностей, JSON-RPC-сообщения под капотом — распакуем в следующей главе. Пока держи в голове результат, а не формат проводов.

Скиллы + MCP

Важная оговорка: MCP появился раньше скиллов, и скиллы со временем «съели» часть его функциональности. Тем не менее MCP остаётся мощным способом расширять возможности агента — особенно там, где нужна аутентификация.

  • Скиллы идеальны, чтобы научить агента, как что-то делать (писать промпты, разбирать письма, следовать код-стайлу).
  • MCP идеален, чтобы дать агенту возможность что-то делать (читать файлы, дёргать API, ходить в базы).

Чаще всего они работают вместе: скиллы дают знания, MCP — инструменты и данные.

Что будем строить

В этом юните: архитектура MCP (хосты, клиенты, серверы и три типа возможностей), сборка серверов на FastMCP и Gradio, настройка агентов как MCP-клиентов, деплой на Hugging Face Spaces и hands-on-проект, который свяжет всё вместе.

Юнит 2 · MCP

Ключевые концепции и архитектура MCP

В MCP три роли — хосты, клиенты и серверы, — которые обмениваются тремя видами возможностей: инструментами (tools), ресурсами (resources) и промптами (prompts).

Три роли

Хост — окружение, в котором крутится кодовый агент: Claude Code (CLI), Codex, OpenCode или твой собственный Python-скрипт с AI SDK. Хост управляет подключением MCP-клиента и показывает результаты пользователю. Сам протокол хост не реализует — он делегирует это клиенту.

Клиент — обработчик протокола внутри хоста, который общается с MCP-серверами. Он находит доступные серверы и их возможности, шлёт запросы (вызов инструментов, чтение ресурсов, получение промптов), разбирает ответы и ошибки, держит жизненный цикл соединения и, если нужно, занимается аутентификацией. На практике клиент встроен в рантайм агента — как встроенная поддержка MCP в Claude Code или MCP-клиент Codex.

Сервер — внешняя программа, которая выставляет возможности по MCP: описывает свои инструменты (со схемами и описаниями), публикует ресурсы (с URI и контентом), реализует промпты (шаблоны инструкций), принимает запросы клиентов и отвечает в формате протокола. Сервером может быть Python-скрипт на твоём ноутбуке, сервис на Hugging Face Spaces, контейнер или кусок твоего приложения (например, Gradio-апп с mcp_server=True).

Три типа возможностей

Инструменты (tools) — вызываемые функции, которыми агент совершает действия или достаёт данные. У каждого инструмента есть уникальное имя (типа search_github), описание, JSON Schema входных параметров и сама реализация. Когда агент решает, что инструмент поможет в текущей задаче, он вызывает его с параметрами, сервер исполняет и возвращает результат, агент встраивает его в свои рассуждения. Инструменты — model-controlled: агент сам решает, звать их или нет. Они могут менять состояние (писать файлы, слать сообщения), содержат обработку ошибок и возвращают структурированные или сырые результаты.

Ресурсы (resources) — источники данных только для чтения. У каждого — URI (вроде file:///path/to/code.py), человекочитаемое имя, описание и MIME-тип. Когда агенту нужна информация для рассуждений, он запрашивает ресурс по URI, сервер отдаёт контент, агент использует его как контекст. В отличие от инструментов, ресурсы — application-controlled: что выставлять, определяет хост. Они доступны всегда, без явного вызова — идеально для статичных и полустатичных данных: файлов проекта, документации, конфигов.

Промпты (prompts) — шаблоны инструкций, которые агент может запросить, чтобы структурировать собственное поведение в конкретном контексте. У каждого — имя (типа code_review), описание, опциональные аргументы и сам текст шаблона. Промпты — user-controlled: деплоятся отдельно и обновляются независимо. Это неисполняемый текст — идеален для стандартизации повторяющихся задач вроде код-ревью или security-аудитов.

Факт В среднем ~90% использования MCP приходится на инструменты.

Протокол: JSON-RPC поверх транспортов

MCP гоняет сообщения в формате JSON-RPC 2.0: каждый запрос и ответ — JSON-RPC-сообщение. Протокол не привязан к транспорту — один и тот же формат работает по любому каналу.

Определение Транспорт — любой канал связи, по которому едет сообщение: процессы на одной машине (stdio) или сетевое соединение (например, HTTP).

Типичный запрос клиента на вызов инструмента:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search_github",
    "arguments": {
      "query": "code agents"
    }
  }
}

Ответ сервера:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "[результаты поиска по GitHub...]"
      }
    ]
  }
}

MCP определяет конкретные методы: tools/list, tools/call, resources/list, resources/read, prompts/get.

Транспорты

Протокол (JSON-RPC) отделён от транспорта (как сообщения ходят между клиентом и сервером). Стандартных транспортов два.

Stdio

Для локальных соединений, когда сервер крутится на твоей машине: сервер запускается сабпроцессом хоста, общение — через stdin/stdout, без сетевого оверхеда. Идеален для разработки и локальных инструментов, самый безопасный (наружу ничего не торчит).

claude mcp add --transport stdio my-server -- python /path/to/server.py
codex mcp add my-server -- python /path/to/server.py
opencode mcp add my-server --command "python" --args "/path/to/server.py"

У Pi нет встроенного MCP-клиента. Ставится pi-mcp-adapter, который читает стандартный .mcp.json:

pi install npm:pi-mcp-adapter
{
  "mcpServers": {
    "my-server": {
      "command": "python",
      "args": ["/path/to/server.py"]
    }
  }
}

Streamable HTTP

Для удалённых соединений, когда сервер живёт где-то ещё: клиент шлёт HTTP POST на единственный эндпоинт, сервер отвечает JSON-RPC-результатами или SSE-стримами. Работает через интернет и файрволы, идеален для облачных деплоев и общих ресурсов, поддерживает auth-заголовки и управление сессиями.

Streamable HTTP — текущий стандарт для удалённых MCP-серверов, введён в ревизии протокола 2025-03-26 и сохранён в последующих. Старый транспорт HTTP+SSE из ревизии 2024-11-05 объявлен устаревшим, но пока поддерживается для обратной совместимости.

claude mcp add hf-mcp-server -t http "https://huggingface.co/mcp?login"
codex mcp add hf-mcp-server --url "https://huggingface.co/mcp?login"
{
  "mcp": {
    "remote-server": {
      "type": "remote",
      "url": "https://your-server.example.com/mcp"
    }
  }
}

С установленным pi-mcp-adapter удалённые серверы задаются в .mcp.json:

{
  "mcpServers": {
    "hf-mcp-server": {
      "url": "https://huggingface.co/mcp?login"
    }
  }
}

Внутри Pi открой /mcp, чтобы посмотреть сервер или подключиться к нему по требованию.

АспектStdioStreamable HTTP
Где серверЛокальноУдалённо
СетьНе нужнаИнтернет
НастройкаПростаяПростая
СкоростьОчень быстроСетевые задержки
БезопасностьНичего не торчит наружуНужна аутентификация
СценарийРазработка, один пользовательПродакшн, команды, облако

Как всё складывается вместе

┌─────────────────────────────────────────┐
│         Кодовый агент (хост)            │
│    (Claude Code, Codex, OpenCode)       │
└──────────┬──────────────────────────────┘
           │
           │ MCP-клиент
           │ JSON-RPC-сообщения
           │
    ┌──────┴─────────┬──────────────┬──────────────┐
    │                │              │              │
    ▼                ▼              ▼              ▼
┌────────────┐  ┌────────────┐  ┌────────────┐  ┌────────────┐
│  GitHub    │  │   Slack    │  │ Внутренний │  │  Проектный │
│ MCP-сервер │  │ MCP-сервер │  │ API-сервер │  │ MCP-сервер │
│            │  │            │  │            │  │            │
│ Tools:     │  │ Tools:     │  │ Tools:     │  │ Resources: │
│ -list_prs  │  │ -send_msg  │  │ -query_db  │  │ -code.py   │
│ -search    │  │ -list_msgs │  │ -update    │  │ -docs.md   │
└────────────┘  └────────────┘  └────────────┘  └────────────┘

Клиент агента может общаться с несколькими серверами одновременно и комбинировать их возможности.

Tools vs Resources vs Prompts

АспектToolsResourcesPrompts
Кто управляетМодельПриложениеПользователь
МутабельностьМогут менять состояниеТолько чтениеСтатичны
ВызовАгент решает когдаАгент запрашивает по URIПредоставляет хост
СценарийДействия, функцииДанные, контекстИнструкции
ПримерВызов APIЧтение файлаШаблон код-ревью

Жизненный цикл MCP

Интерактив · жизненный цикл MCP-соединения

Хост (агент)
MCP-клиент
MCP-сервер
1/5

Серверы могут запускаться по требованию (stdio), работать постоянно (Streamable HTTP), динамически добавляться и удаляться, а для Streamable HTTP — обновляться без перезапуска хоста.

Ключевое Хосты запускают агентов, клиенты запрашивают возможности, серверы их предоставляют. Tools, resources и prompts покрывают всё, что нужно агенту. Сообщения возит JSON-RPC; stdio — локальный транспорт, Streamable HTTP — удалённый. Вместе это решает проблему интеграций M×N одним протоколом.

Дальше — собираем свой первый MCP-сервер.

Юнит 2 · MCP

Строим MCP-серверы на Python

Архитектуру разобрали — теперь строим настоящие серверы: сначала лёгкие на FastMCP SDK, потом на Gradio, где сервер идёт в комплекте с веб-интерфейсом.

Ставим FastMCP

FastMCP — самый простой способ собрать MCP-сервер:

pip install "mcp[cli]"

Пакет mcp включает FastMCP SDK; экстра [cli] добавляет команду mcp, которая пригодится для тестирования ниже.

Первый сервер: калькулятор

Создай calculator_server.py:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("calculator")

@mcp.tool()
def add(a: int, b: int) -> int:
    """Сложить два числа."""
    return a + b

@mcp.tool()
def multiply(a: int, b: int) -> int:
    """Перемножить два числа."""
    return a * b

if __name__ == "__main__":
    mcp.run()

FastMCP автоматически: выводит типы параметров из сигнатур функций, генерирует JSON-схемы, превращает докстринги в описания инструментов, по умолчанию использует stdio-транспорт и берёт весь JSON-RPC на себя.

python calculator_server.py

Сервер слушает JSON-RPC-запросы на stdin/stdout.

Инструменты посложнее

Соберём сервер, который читает и анализирует файлы:

import os

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("file-analyzer")

@mcp.tool()
def read_file(path: str) -> str:
    """Прочитать содержимое файла.

    Args:
        path: абсолютный путь к файлу

    Returns:
        Содержимое файла строкой
    """
    try:
        with open(path, "r") as f:
            return f.read()
    except FileNotFoundError:
        return f"Ошибка: файл не найден: {path}"
    except Exception as e:
        return f"Ошибка чтения файла: {e}"

@mcp.tool()
def count_lines(path: str) -> int:
    """Посчитать количество строк в файле.

    Args:
        path: абсолютный путь к файлу

    Returns:
        Число строк в файле
    """
    try:
        with open(path, "r") as f:
            return len(f.readlines())
    except Exception:
        return -1

@mcp.tool()
def list_directory(path: str) -> list[str]:
    """Список файлов в каталоге.

    Args:
        path: путь к каталогу

    Returns:
        Список имён файлов
    """
    try:
        return os.listdir(path)
    except Exception:
        return []

if __name__ == "__main__":
    mcp.run()

Ключевые особенности:

  • Докстринги становятся описаниями — первая строка описывает инструмент, дополнительные секции — параметры.
  • Тайп-хинты дают валидацию — параметры проверяются по типам автоматически.
  • Обработка ошибок — возвращай осмысленные сообщения вместо исключений.
  • Типы возврата — FastMCP сам понимает, что вернуть.

Добавляем ресурсы

Ресурсы — данные только для чтения, декоратор @mcp.resource():

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("documentation")

@mcp.resource("doc://api/overview")
def api_overview() -> str:
    """Обзор API и гайд для старта."""
    return """# Обзор API

API даёт инструменты для управления пользователями,
запросов к данным и генерации отчётов.

## Как начать
1. Аутентифицируйся своим API-токеном
2. Вызывай эндпоинты с нужными параметрами
3. Аккуратно обрабатывай ответы и ошибки

## Лимиты
- 100 запросов в минуту на API-ключ
- Всплеск: до 10 запросов в секунду
"""

@mcp.resource("doc://api/endpoints")
def api_endpoints() -> str:
    """Полный список эндпоинтов API."""
    return """# Эндпоинты

## Пользователи
- GET /users — список пользователей
- POST /users — создать пользователя
- GET /users/{id} — получить пользователя
- PUT /users/{id} — обновить пользователя

## Данные
- POST /query — выполнить запрос к базе
- GET /data/{id} — получить данные

## Отчёты
- GET /reports — список отчётов
- POST /reports — сгенерировать отчёт
"""

@mcp.tool()
def get_api_status() -> dict:
    """Проверить текущий статус API."""
    return {
        "status": "operational",
        "uptime_percent": 99.99,
        "response_time_ms": 45,
    }

if __name__ == "__main__":
    mcp.run()

Ресурсы: идентифицируются URI (например, doc://api/overview), отдают статичный или полустатичный контент (текст, JSON, любая строка), не принимают параметров (в отличие от инструментов) и всегда доступны агентам.

Добавляем промпты

Промпты — шаблоны инструкций, направляющие поведение агента:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("prompts-server")

@mcp.prompt()
def code_review_prompt(language: str = "python") -> str:
    """Шаблон ревью кода на заданном языке."""
    return f"""Ты — опытный ревьюер кода на {language}.
Проанализируй код и дай фидбек по пунктам:
1. Корректность и логика
2. Производительность
3. Стиль и читаемость
4. Последствия для безопасности
5. Покрытие тестами

Будь конструктивен, предлагай улучшения."""

@mcp.prompt()
def security_audit_prompt() -> str:
    """Шаблон security-аудита."""
    return """Проведи аудит безопасности кода или системы. Проверь:
1. Уязвимости аутентификации
2. Проблемы авторизации
3. Пробелы в валидации данных
4. SQL-инъекции и прочие инъекции
5. Небезопасные зависимости
6. Утечки чувствительной информации

Каждой находке присвой severity: critical, high, medium, low."""

@mcp.tool()
def get_available_prompts() -> list[str]:
    """Список доступных шаблонов промптов."""
    return ["code_review_prompt", "security_audit_prompt"]

if __name__ == "__main__":
    mcp.run()

Обработка ошибок в FastMCP

Обрабатывай ошибки мягко — возвращай сообщения об ошибках:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("safe-operations")

@mcp.tool()
def divide(numerator: float, denominator: float) -> str:
    """Поделить два числа.

    Args:
        numerator: делимое
        denominator: делитель

    Returns:
        Частное или сообщение об ошибке
    """
    try:
        if denominator == 0:
            return "Ошибка: на ноль делить нельзя"
        return f"Результат: {numerator / denominator}"
    except Exception as e:
        return f"Ошибка: {e}"

@mcp.tool()
def parse_json(data: str) -> str:
    """Распарсить JSON-строку.

    Args:
        data: JSON-строка

    Returns:
        Разобранный JSON или сообщение об ошибке
    """
    import json
    try:
        parsed = json.loads(data)
        return f"Валидный JSON: {parsed}"
    except json.JSONDecodeError as e:
        return f"Невалидный JSON: {e}"

if __name__ == "__main__":
    mcp.run()

Бест-практики: возвращай сообщения об ошибках вместо исключений, добавляй в них полезный контекст, валидируй входные параметры до использования, логируй фейлы для отладки.

Локальное тестирование

Запуск напрямую: python calculator_server.py поднимает сервер на stdio. Можно слать JSON-RPC руками в stdin, но обычно удобнее инспектор.

MCP Inspector (рекомендуется) — официальный веб-UI, который подключается к любому MCP-серверу и позволяет вызывать инструменты, читать ресурсы и получать промпты. Через CLI из mcp[cli]:

mcp dev calculator_server.py

Или напрямую через npx:

npx @modelcontextprotocol/inspector python calculator_server.py

Оба варианта открывают инспектор в браузере и подключают его к серверу по stdio.

Gradio-интеграция (превью)

FastMCP — лучший дефолт для чистого MCP-сервера. Gradio полезен, когда те же функции должны быть доступны и агентам, и людям в браузере.

pip install "gradio[mcp]"
import gradio as gr

def letter_counter(word: str, letter: str) -> int:
    """Посчитать вхождения буквы в текст.

    Args:
        word: входной текст
        letter: искомая буква

    Returns:
        Количество вхождений
    """
    return word.lower().count(letter.lower())

def reverse_text(text: str) -> str:
    """Развернуть строку.

    Args:
        text: входной текст

    Returns:
        Перевёрнутый текст
    """
    return text[::-1]

with gr.Blocks() as demo:
    with gr.Tab("Счётчик букв"):
        word_input = gr.Textbox(label="Текст")
        letter_input = gr.Textbox(label="Буква")
        count_output = gr.Number(label="Количество")
        gr.Button("Посчитать").click(letter_counter, [word_input, letter_input], count_output)

    with gr.Tab("Разворот текста"):
        text_input = gr.Textbox(label="Текст")
        reversed_output = gr.Textbox(label="Наоборот")
        gr.Button("Развернуть").click(reverse_text, [text_input], reversed_output)

if __name__ == "__main__":
    demo.launch(mcp_server=True)

Запуск с mcp_server=True автоматически: превращает функции в MCP-инструменты, поднимает веб-UI на http://localhost:7860, выставляет MCP-эндпоинт на http://localhost:7860/gradio_api/mcp/ и берёт JSON-RPC-сериализацию на себя. Твои функции теперь и компоненты веб-интерфейса, И MCP-инструменты.

Заметка Инструменты с UI полезны тем, что обычные люди могут работать с теми же функциями в браузере, а у тебя появляется быстрый «человеческий» стенд для тестов. Если сетап только для агентов — UI не обязателен.

Про @gr.mcp.resource(), @gr.api(), деплой и аутентификацию — в отдельной главе про Gradio дальше. Из этой страницы достаточно главного: mcp_server=True делает из одних и тех же функций и браузерные обработчики, и MCP-инструменты.

Ключевое FastMCP превращает докстринги в описания инструментов, а тайп-хинты — в JSON-схемы. Tools — вызываемые функции, resources — данные для чтения по URI, prompts — шаблоны инструкций. Сначала доведи «чистый» сервер: обработка ошибок, локальные тесты, — и только потом решай, нужен ли браузерный UI. Если нужен — Gradio добавит его одним параметром.

Дальше — наводим агентов на эти серверы.

Юнит 2 · Квиз

Квиз 1: основы MCP

Проверяем понимание архитектуры MCP и ключевых концепций.

В чём суть проблемы интеграций M×N?

Как распределены роли хоста, клиента и сервера?

Чем отличаются tools, resources и prompts?

Какие транспорты и для чего использует MCP?

В чём разница между скиллами и MCP?

Итоги

Если 4–5 верных — база MCP у тебя крепкая. Если было тяжело — перечитай «Ключевые концепции и архитектуру».

Запомнить: MCP решает проблему M×N универсальным стандартом серверов; три роли — хост (где агент), клиент (обработчик протокола), сервер (поставщик возможностей); три возможности — tools (вызываемые), resources (данные), prompts (инструкции); два транспорта — stdio (локальная разработка) и Streamable HTTP (продакшн); скиллы и MCP работают в паре — одни учат, другой даёт возможности.

Юнит 2 · MCP

Настраиваем агентов как MCP-клиентов

Claude Code, Codex и OpenCode — все являются MCP-клиентами. Твоя задача — сказать каждому, к каким серверам подключаться и как.

Когда MCP-сервер настроен, агент подключается к нему, узнаёт, какие инструменты и ресурсы тот выставляет, и обращается с ними как со встроенными возможностями. Обнаружение, маршрутизацию запросов и ошибки MCP-клиент агента разруливает за кадром.

Добавляем MCP-сервер

Команда claude mcp add. Опции идут до имени сервера:

Stdio (локальный) сервер:

claude mcp add --transport stdio <name> -- <command> [args]

HTTP (удалённый) сервер:

claude mcp add --transport http <name> <url>

Примеры:

# Локальный Python-сервер
claude mcp add --transport stdio calculator -- python /path/to/server.py

# Удалённый HTTP-сервер
claude mcp add --transport http my-api https://api.example.com/mcp

# Со скоупом и переменными окружения
claude mcp add --transport stdio --scope user --env API_KEY=xxx github -- npx -y @modelcontextprotocol/server-github

Команда codex mcp add или правка config.toml напрямую:

codex mcp add <name> --env VAR=VALUE -- <command> [args]

config.toml~/.codex/config.toml или .codex/config.toml):

# Stdio-сервер
[mcp_servers.calculator]
command = "python"
args = ["/path/to/server.py"]

# HTTP-сервер
[mcp_servers.my-api]
url = "https://api.example.com/mcp"

# С переменными окружения
[mcp_servers.github]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]

[mcp_servers.github.env]
GITHUB_TOKEN = "ghp_xxxxx"

Правь opencode.json в корне проекта:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "calculator": {
      "type": "local",
      "command": ["python", "/path/to/server.py"]
    },
    "my-api": {
      "type": "remote",
      "url": "https://api.example.com/mcp"
    },
    "github": {
      "type": "local",
      "command": ["npx", "-y", "@modelcontextprotocol/server-github"],
      "environment": {
        "GITHUB_TOKEN": "ghp_xxxxx"
      }
    }
  }
}

Pi использует пакет pi-mcp-adapter вместо встроенного клиента. Ставится один раз:

pi install npm:pi-mcp-adapter

Затем серверы добавляются в .mcp.json (проект) или ~/.config/mcp/mcp.json (общий пользовательский конфиг):

{
  "mcpServers": {
    "calculator": {
      "command": "python",
      "args": ["/path/to/server.py"]
    },
    "my-api": {
      "url": "https://api.example.com/mcp"
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_TOKEN": "ghp_xxxxx"
      }
    }
  }
}

Управление серверами

# Список настроенных серверов
claude mcp list

# Детали конкретного сервера
claude mcp get <name>

# Удалить сервер
claude mcp remove <name>

# Статус внутри сессии
/mcp

Внутри сессии — /mcp для просмотра активных серверов. Можно править ~/.codex/config.toml напрямую; enabled = false временно выключает сервер, не удаляя его:

[mcp_servers.my-api]
url = "https://api.example.com/mcp"
enabled = false
# Список серверов и статус авторизации
opencode mcp list

# Отладка проблем с подключением
opencode mcp debug <name>

# Аутентификация на сервере
opencode mcp auth <name>

# Удалить сохранённые креды
opencode mcp logout <name>

"enabled": false в opencode.json выключает сервер без удаления.

/mcp
/mcp tools
/mcp reconnect
/mcp reconnect <server>
/mcp-auth <server>

/mcp — интерактивная панель, /mcp tools — список обнаруженных инструментов, /mcp reconnect — переподключение лениво загружаемого сервера.

Скоупы конфигурации

Три скоупа, флаг --scope:

  • local (по умолчанию) — только текущий проект, приватно для тебя (хранится в ~/.claude.json);
  • project — шарится с командой через version control (файл .mcp.json в корне проекта);
  • user — доступен во всех твоих проектах (хранится в ~/.claude.json).
# На уровне пользователя (доступно везде)
claude mcp add --transport http --scope user my-api https://api.example.com/mcp

# На уровне проекта (шарится с командой)
claude mcp add --transport http --scope project my-api https://api.example.com/mcp

Два скоупа:

  • Глобальный~/.codex/config.toml (все проекты);
  • Проектный.codex/config.toml (только в доверенных проектах).

CLI и IDE-расширение используют один конфиг.

Два скоупа:

  • Проектopencode.json в корне;
  • Организация — дефолтные серверы с эндпоинта .well-known/opencode, к которым пользователи могут подключиться.

Локальные значения переопределяют удалённые дефолты организации.

С pi-mcp-adapter конфиг живёт на четырёх уровнях:

  • Общий пользовательский~/.config/mcp/mcp.json
  • Pi-переопределение пользователя~/.pi/agent/mcp.json
  • Общий проектный.mcp.json
  • Pi-переопределение проекта.pi/mcp.json

Проектные файлы важнее глобальных. Для репо-шаринга предпочитай .mcp.json, а .pi/mcp.json — только для Pi-специфичных переопределений.

Транспорты

Все агенты поддерживают два основных транспорта: stdio — локальный сабпроцесс через stdin/stdout, лучший выбор для дев-серверов и инструментов с прямым доступом к системе; Streamable HTTP — удалённые соединения по HTTP, текущий стандарт для облачных серверов, работает через интернет и файрволы. SSE — легаси-транспорт: устарел, но пока поддерживается для совместимости.

Экосистема MCP от Hugging Face

Hugging Face предоставляет официальные MCP-серверы для интеграции с Hub. Настраиваются в любом MCP-совместимом агенте:

claude mcp add --transport http --scope user hf-mcp "https://huggingface.co/mcp?login"
# В ~/.codex/config.toml
[mcp_servers.hf-mcp]
url = "https://huggingface.co/mcp?login"
{
  "mcp": {
    "hf-mcp": {
      "type": "remote",
      "url": "https://huggingface.co/mcp?login"
    }
  }
}
{
  "mcpServers": {
    "hf-mcp": {
      "url": "https://huggingface.co/mcp?login"
    }
  }
}

HF MCP-сервер даёт агентам поиск моделей и датасетов, чтение файлов репозиториев, запросы к карточкам моделей и навигацию по контенту Hub.

Совет Загляни на huggingface.co/settings/mcp — там официальные MCP-серверы и примеры конфигурации под твоего агента.

Удалённые MCP-серверы

Для серверов на Hugging Face Spaces и других облачных платформах используй Streamable HTTP:

claude mcp add --transport http --scope user sentiment-analyzer https://username-sentiment.hf.space/mcp

claude mcp add --transport http --scope user code-reviewer https://username-reviewer.hf.space/mcp
# В ~/.codex/config.toml
[mcp_servers.sentiment-analyzer]
url = "https://username-sentiment.hf.space/mcp"

[mcp_servers.code-reviewer]
url = "https://username-reviewer.hf.space/mcp"
{
  "mcp": {
    "sentiment-analyzer": {
      "type": "remote",
      "url": "https://username-sentiment.hf.space/mcp"
    },
    "code-reviewer": {
      "type": "remote",
      "url": "https://username-reviewer.hf.space/mcp"
    }
  }
}
{
  "mcpServers": {
    "sentiment-analyzer": {
      "url": "https://username-sentiment.hf.space/mcp"
    },
    "code-reviewer": {
      "url": "https://username-reviewer.hf.space/mcp"
    }
  }
}

Удалённые серверы по Streamable HTTP не требуют перезапуска агента, работают через интернет и файрволы, обновляются независимо и поддерживают опциональную аутентификацию через заголовки.

Отладка MCP-конфигураций

Если сервер не работает, проверь по порядку:

1. Список настроенных серверов:

claude mcp list
claude mcp get <server-name>

Проверь ~/.codex/config.toml или запусти /mcp в сессии.

opencode mcp list
opencode mcp debug <server-name>
/mcp
/mcp tools
/mcp reconnect <server-name>

Плюс сверь общие конфиги, которые читает Pi:

cat .mcp.json
cat .pi/mcp.json

2. Запусти сервер руками: python /path/to/server.py — если сервер падает, ошибку увидишь сразу.

3. Проверь переменные окружения: echo $GITHUB_TOKEN — если пусто, у сервера нет нужных секретов.

4. Проверь URL удалённого сервера: curl "https://your-space.hf.space/mcp" — работающий сервер должен вернуть валидный ответ.

Бест-практики конфигурации

Секреты и API-ключи — только через переменные окружения. Пути к локальным серверам — абсолютные. Один сервер — одна зона ответственности (GitHub и Slack — раздельно). Сначала тестируй локально, потом деплой в облако. Имена серверов — говорящие. Конфигурацию документируй и держи в version control — но без секретов.

Ключевое Claude Code — claude mcp add, Codex — codex mcp add или config.toml, OpenCode — opencode.json, Pi — pi-mcp-adapter плюс .mcp.json. Паттерн одинаковый: имя сервера, транспорт и команда (stdio) или URL (Streamable HTTP). Секреты — в env-переменных; серверы тестируй локально, прежде чем дебажить конфиг клиента.

Дальше — глубокое погружение в Gradio.

Юнит 2 · MCP

Gradio MCP: веб-интерфейсы + MCP-серверы

Эта глава продолжает предыдущую: минимальный паттерн mcp_server=True ты уже видел. Здесь разберёмся, когда Gradio — хороший выбор, как выставлять MCP-only функции и как безопасно всё это деплоить.

Что такое Gradio MCP-интеграция

Gradio — Python-библиотека для веб-интерфейсов к ML-моделям и инструментам обработки данных. С включённым MCP Gradio: автоматически конвертирует твои функции в MCP-инструменты, создаёт веб-интерфейс для людей, выставляет MCP-эндпоинт для агентов и разруливает JSON-RPC-сериализацию и детали протокола.

Два интерфейса по цене одного: люди — через браузер, агенты — через MCP.

Установка

pip install "gradio[mcp]"

И requirements.txt для деплоя:

gradio>=4.0.0
Совет Тайп-хинты и докстринги — обязательны: из них Gradio генерирует схему MCP-инструмента. Всегда указывай типы всех параметров и возврата, описание в докстринге, секцию Args: по каждому параметру и Returns: для результата.

Практичное Gradio MCP-приложение

Вместо очередного hello-world — небольшой текстовый тулкит, полезный и в браузере, и через MCP:

import json

import gradio as gr

def analyze_text(text: str) -> str:
    """Проанализировать текст и посчитать статистику.

    Args:
        text: входной текст

    Returns:
        JSON с результатами анализа
    """
    words = text.split()
    chars = len(text)

    return json.dumps({
        "words": len(words),
        "characters": chars,
        "average_word_length": round(chars / len(words), 2) if words else 0,
    })

def reverse_text(text: str) -> str:
    """Развернуть строку.

    Args:
        text: входной текст

    Returns:
        Перевёрнутый текст
    """
    return text[::-1]

def count_vowels(text: str) -> int:
    """Посчитать гласные в тексте.

    Args:
        text: входной текст

    Returns:
        Число гласных
    """
    vowels = "aeiouAEIOU"
    return sum(1 for char in text if char in vowels)

with gr.Blocks(title="Text Tools") as demo:
    gr.Markdown("# Инструменты обработки текста")

    with gr.Tab("Анализ текста"):
        text_input1 = gr.Textbox(label="Текст", lines=5)
        analysis_output = gr.Textbox(label="Анализ", lines=5)
        gr.Button("Проанализировать").click(analyze_text, text_input1, analysis_output)

    with gr.Tab("Разворот текста"):
        text_input2 = gr.Textbox(label="Текст", lines=5)
        reverse_output = gr.Textbox(label="Наоборот", lines=5)
        gr.Button("Развернуть").click(reverse_text, text_input2, reverse_output)

    with gr.Tab("Счётчик гласных"):
        text_input3 = gr.Textbox(label="Текст")
        vowel_output = gr.Number(label="Гласных")
        gr.Button("Посчитать").click(count_vowels, text_input3, vowel_output)

if __name__ == "__main__":
    demo.launch(mcp_server=True)

Каждая функция становится отдельным MCP-инструментом, который агент может вызвать независимо.

Продвинутые фичи

Ресурсы через @gr.mcp.resource()

Данные только для чтения, доступные агентам:

import gradio as gr

@gr.mcp.resource("config://api")
def api_documentation() -> str:
    """Документация API и эндпоинты."""
    return """# Документация API

## Эндпоинты
- GET /users — список пользователей
- POST /users — создать пользователя
- GET /users/{id} — получить пользователя

## Аутентификация
Bearer-токен в заголовке Authorization.
"""

# Дальше — твои инструменты и UI...

if __name__ == "__main__":
    demo.launch(mcp_server=True)

Ресурсы идентифицируются URI и отдают статичный или полустатичный контент.

MCP-only функции через @gr.api()

Инструменты, которые существуют только в MCP и не попадают в веб-UI:

import gradio as gr

@gr.api()
def query_database(sql: str) -> str:
    """Выполнить запрос к базе (только MCP).

    Args:
        sql: SQL-запрос

    Returns:
        Результаты запроса
    """
    # Доступно только по MCP, не из веб-UI
    return "Результаты запроса..."

@gr.api()
def send_email(to: str, subject: str, body: str) -> str:
    """Отправить письмо (только MCP).

    Args:
        to: адрес получателя
        subject: тема письма
        body: текст письма

    Returns:
        Подтверждение отправки
    """
    return f"Письмо отправлено: {to}"

# Обычные UI-компоненты здесь...

if __name__ == "__main__":
    demo.launch(mcp_server=True)

Аутентификация

Аргумент auth у launch() закрывает базовой HTTP-аутентификацией только веб-UI. MCP-эндпоинт /gradio_api/mcp/ он не защищает — так задумано, агенты достучатся без логина.

import gradio as gr

def authenticate(username: str, password: str) -> bool:
    return username == "admin" and password == "secret"

# `auth` защищает только веб-UI; MCP остаётся открытым.
if __name__ == "__main__":
    demo.launch(mcp_server=True, auth=authenticate)

Для продакшна аутентифицируй MCP-эндпоинт вне Gradio: поставь приложение за реверс-прокси, который проверяет доступ к /gradio_api/mcp/; задеплой приватный Hugging Face Space; или используй MCP-гейтвей, реализующий спецификацию MCP Authorization.

Деплой на Hugging Face Spaces

Spaces бесплатно хостит Gradio-приложения с поддержкой MCP.

  1. Создай Space: иди на huggingface.co/new-space, назови Space, выбери SDK «Gradio», создай.
  2. Загрузи кодapp.py:
    import gradio as gr
    
    def my_tool(input_text: str) -> str:
        """Сделать что-то с входом.
    
        Args:
            input_text: входные данные
    
        Returns:
            Результат
        """
        return f"Обработано: {input_text}"
    
    demo = gr.Interface(fn=my_tool, inputs="text", outputs="text")
    
    if __name__ == "__main__":
        demo.launch(mcp_server=True)
  3. Добавь зависимости (если нужны) в requirements.txt — Space ставит их автоматически при каждой сборке:
    gradio>=4.0.0
    requests>=2.28.0
  4. Забирай MCP-эндпоинт. После деплоя сервер доступен по адресу:
    https://[username]-[space-name].hf.space/gradio_api/mcp/

Настрой агента на него:

claude mcp add --transport http --scope user my-tools https://yourname-space-name.hf.space/gradio_api/mcp/
# В ~/.codex/config.toml
[mcp_servers.my-tools]
url = "https://yourname-space-name.hf.space/gradio_api/mcp/"
{
  "mcp": {
    "my-tools": {
      "type": "remote",
      "url": "https://yourname-space-name.hf.space/gradio_api/mcp/"
    }
  }
}

С установленным pi-mcp-adapter добавь эндпоинт Space в .mcp.json:

{
  "mcpServers": {
    "my-tools": {
      "url": "https://yourname-space-name.hf.space/gradio_api/mcp/"
    }
  }
}

Открой pi и проверь через /mcp tools, что адаптер видит сгенерированные Gradio инструменты.

Совет Эндпоинт Space работает по Streamable HTTP — изменения конфигурации подхватываются сразу, без перезапуска агента.

Бест-практики сигнатур функций

# Хорошо: явные типы, докстринг, секции Args/Returns
def process_data(data: str, format: str = "json") -> str:
    """Обработать данные в заданном формате.

    Args:
        data: входная строка данных
        format: формат вывода (по умолчанию json)

    Returns:
        Обработанные данные строкой
    """
    return data

# Хорошо: несколько параметров, всё описано
def calculate(a: float, b: float, operation: str) -> float:
    """Выполнить математическую операцию.

    Args:
        a: первое число
        b: второе число
        operation: операция (add, subtract, multiply, divide)

    Returns:
        Результат операции
    """
    if operation == "add":
        return a + b
    # ... остальные операции

# Плохо: нет тайп-хинтов
def bad_function(data):  # и нет типа возврата!
    return data

# Плохо: нет докстринга
def also_bad(text: str) -> str:
    return text.upper()

# Плохо: сложные типы без пояснений
def confusing(data: dict) -> list:  # что внутри dict и list?
    return []

Правила: простые понятные типы (str, int, float, bool, list); докстринг с описанием; каждый параметр — в Args; результат — в Returns; возвращай строки, числа или списки; для сложных данных — JSON-строки.

Траблшутинг

Функции не появляются как MCP-инструменты

Проверь сигнатуры: тайп-хинты на всех параметрах, докстринг присутствует, в нём есть секция Args:, тип возврата указан.

# Так работает
def my_tool(text: str) -> str:
    """Обработать текст.

    Args:
        text: входной текст

    Returns:
        Обработанный текст
    """
    return text.upper()

# Так не работает (нет докстринга)
def bad_tool(text: str) -> str:
    return text.upper()

MCP-эндпоинт не отвечает

  1. Проверь, что Space запущен (не в состоянии ошибки).
  2. Используй правильный эндпоинт: https://user-space.hf.space/gradio_api/mcp/.
  3. Проверь curl-ом: curl https://user-space.hf.space/gradio_api/mcp/.
  4. Посмотри логи Space на предмет ошибок.

Ошибки несоответствия типов

Агенты могут падать, если фактический тип возврата не совпадает с заявленным:

# Правильно: заявлена строка — возвращается строка
def correct(x: int) -> str:
    return str(x * 2)

# Неправильно: заявлена строка, а возвращается int
def wrong(x: int) -> str:
    return x * 2  # int, а не строка!

Производительность в продакшне

  1. Функции должны быть быстрыми — агенты отваливаются по таймауту ~60 секунд.
  2. Валидируй вход — проверяй размер данных до обработки.
  3. Обрабатывай ошибки мягко — сообщение об ошибке вместо падения.
  4. Кешируй дорогие результаты — не считай одно и то же дважды.
  5. Следи за ресурсами — у Spaces ограничены CPU и память.
import time

def analyze(data: str) -> str:
    """Анализ больших датасетов.

    Args:
        data: входные данные

    Returns:
        Результаты анализа
    """
    # Отбрасываем слишком большой вход
    if len(data) > 1_000_000:
        return "Ошибка: вход слишком большой (максимум 1 МБ)"

    start = time.time()
    # ... сам анализ
    elapsed = time.time() - start

    if elapsed > 30:
        return "Ошибка: обработка заняла слишком долго"

    return "Результаты..."
Ключевое mcp_server=True даёт веб-UI и MCP-сервер из одного Gradio-приложения. Схемы инструментов строятся из тайп-хинтов и докстрингов — держи их в тонусе. Spaces — быстрый способ публичного хостинга; @gr.api() прячет инструменты из UI, @gr.mcp.resource() выставляет данные для чтения.

Дальше собираем всё это в hands-on-проект.

Юнит 2 · Практика

Практика: собери и задеплой MCP-сервер

В этом проекте ты соберёшь полноценный MCP-сервер с инструментами обработки текста, протестируешь локально и задеплоишь на Hugging Face Spaces.

Сервер даст кодовым агентам: анализ статистики текста (слова, символы и т.д.), извлечение ключевых слов, оценку сложности чтения и вспомогательные операции. Это реальный расшариваемый инструмент, работающий с любым MCP-совместимым агентом.

Часть 1. Локальный сервер

Шаг 1. Настройка проекта

mkdir text-processor-mcp
cd text-processor-mcp
python -m venv venv
source venv/bin/activate  # на Windows: venv\Scripts\activate
pip install "mcp[cli]"

Шаг 2. FastMCP-сервер

Создай server.py с четырьмя инструментами:

import json

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("text-processor")

@mcp.tool()
def analyze_text(text: str) -> str:
    """Проанализировать текст и вернуть статистику.

    Args:
        text: входной текст

    Returns:
        JSON-строка с результатами анализа
    """
    words = text.split()
    chars = len(text)
    chars_no_spaces = len(text.replace(" ", ""))
    sentences = text.count(".") + text.count("!") + text.count("?")

    avg_word_length = round(chars_no_spaces / len(words), 2) if words else 0
    avg_sentence_length = round(len(words) / max(sentences, 1), 2)

    return json.dumps({
        "total_characters": chars,
        "characters_without_spaces": chars_no_spaces,
        "total_words": len(words),
        "total_sentences": max(sentences, 1),
        "average_word_length": avg_word_length,
        "average_sentence_length": avg_sentence_length,
        "unique_words": len(set(w.lower() for w in words)),
    })

@mcp.tool()
def extract_keywords(text: str, count: int = 5) -> str:
    """Извлечь ключевые слова (самые частые) из текста.

    Args:
        text: входной текст
        count: сколько ключевых слов вернуть (по умолчанию 5)

    Returns:
        JSON-строка с ключевыми словами и частотами
    """
    from collections import Counter

    # Отбрасываем стоп-слова
    stopwords = {
        "the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for",
        "of", "with", "is", "are", "was", "were", "be", "been", "by", "from",
    }

    words = text.lower().split()
    filtered = [w.strip(".,!?;:") for w in words if w not in stopwords]

    top_words = Counter(filtered).most_common(count)

    return json.dumps({
        "keywords": [{"word": w, "frequency": f} for w, f in top_words]
    })

@mcp.tool()
def check_reading_level(text: str) -> str:
    """Оценить сложность чтения текста.

    Args:
        text: входной текст

    Returns:
        JSON-строка с оценкой уровня чтения
    """
    sentences = max(text.count(".") + text.count("!") + text.count("?"), 1)
    words = len(text.split())
    syllables = sum(1 for c in text.lower() if c in "aeiou")

    if words == 0:
        return json.dumps({"error": "Нечего анализировать"})

    # Индекс Флеша-Кинкейда
    grade = (0.39 * (words / sentences)) + (11.8 * (syllables / words)) - 15.59
    grade = max(0, round(grade, 1))

    if grade < 6:
        level = "Elementary School"
    elif grade < 9:
        level = "Middle School"
    elif grade < 13:
        level = "High School"
    else:
        level = "College/Academic"

    return json.dumps({
        "grade_level": grade,
        "reading_level": level,
    })

@mcp.tool()
def reverse_text(text: str) -> str:
    """Развернуть строку.

    Args:
        text: входной текст

    Returns:
        Перевёрнутый текст
    """
    return text[::-1]

if __name__ == "__main__":
    mcp.run()

Шаг 3. Локальный тест

python server.py

Сервер слушает stdin/stdout. Открой его в MCP Inspector, чтобы посмотреть и повызывать инструменты:

mcp dev server.py

Инспектор откроется в браузере уже подключённым к серверу — там видны все четыре инструмента, можно их погонять.

Часть 2. Gradio: веб-UI + MCP-сервер

Создай app.py — те же функции (analyze_text, extract_keywords, check_reading_level), обёрнутые в веб-интерфейс:

import json

import gradio as gr

# ... те же три функции с типами и докстрингами, что в server.py
# (analyze_text, extract_keywords, check_reading_level),
# только json.dumps(..., indent=2) для читаемого вывода в UI

with gr.Blocks(title="Text Processor") as demo:
    gr.Markdown("# Инструменты обработки текста")
    gr.Markdown("Статистика текста, ключевые слова, сложность чтения.")

    with gr.Tab("Анализ текста"):
        text_input1 = gr.Textbox(
            label="Текст",
            lines=8,
            placeholder="Вставь текст сюда...",
        )
        analysis_output = gr.Textbox(label="Результаты анализа", lines=8)
        gr.Button("Проанализировать", size="lg").click(
            analyze_text, text_input1, analysis_output
        )

    with gr.Tab("Ключевые слова"):
        text_input2 = gr.Textbox(label="Текст", lines=8)
        count_input = gr.Slider(1, 20, value=5, step=1, label="Сколько слов")
        keywords_output = gr.Textbox(label="Ключевые слова", lines=8)
        gr.Button("Извлечь", size="lg").click(
            extract_keywords, [text_input2, count_input], keywords_output
        )

    with gr.Tab("Уровень чтения"):
        text_input3 = gr.Textbox(label="Текст", lines=8)
        level_output = gr.Textbox(label="Анализ уровня чтения", lines=5)
        gr.Button("Проверить", size="lg").click(
            check_reading_level, text_input3, level_output
        )

if __name__ == "__main__":
    demo.launch(mcp_server=True)

Тестируем:

pip install gradio
python app.py

Веб-UI — на http://localhost:7860, MCP-эндпоинт автоматически доступен на http://localhost:7860/gradio_api/mcp/.

Часть 3. Деплой на Hugging Face Spaces

Шаг 1. Создай Space

  1. Иди на huggingface.co/new-space
  2. Назови его (например, text-processor-mcp)
  3. Выбери SDK «Gradio»
  4. Выбери «Public»
  5. Создай

Шаг 2. Загрузи файлы

app.py — Gradio-версия из части 2, и requirements.txt:

gradio>=4.0.0

Space соберётся и задеплоится автоматически.

Шаг 3. Настрой агента

claude mcp add --transport http --scope user text-processor https://YOUR-USERNAME-text-processor-mcp.hf.space/gradio_api/mcp/
# В ~/.codex/config.toml
[mcp_servers.text-processor]
url = "https://YOUR-USERNAME-text-processor-mcp.hf.space/gradio_api/mcp/"
{
  "mcp": {
    "text-processor": {
      "type": "remote",
      "url": "https://YOUR-USERNAME-text-processor-mcp.hf.space/gradio_api/mcp/"
    }
  }
}

Поставь адаптер один раз:

pi install npm:pi-mcp-adapter

Добавь задеплоенный сервер в .mcp.json:

{
  "mcpServers": {
    "text-processor": {
      "url": "https://YOUR-USERNAME-text-processor-mcp.hf.space/gradio_api/mcp/"
    }
  }
}

Открой pi и проверь сервер и инструменты через /mcp перед промптами.

Шаг 4. Тест с агентом

Перезапусти агента и попроси его:

  • «Проанализируй читаемость этого текста: [текст]»
  • «Какие ключевые слова в этой статье?»
  • «Какой уровень чтения у этого отрывка?»

Агент будет обрабатывать текст через твой MCP-сервер в реальном времени.

Отладка

  1. Логи Space — кнопка «Logs» в интерфейсе Space.
  2. Тест веб-UI — убедись, что Gradio-приложение живо на https://your-space.hf.space.
  3. Проверка MCP-эндпоинта — открой https://your-space.hf.space/gradio_api/mcp/ в браузере.
  4. Сигнатуры функций — у всех есть тайп-хинты и докстринги?
  5. Сначала локально — дебажь на своей машине до деплоя.

Челленджи на прокачку

  1. Сентимент-анализ — определение позитивного/негативного тона.
  2. Определение языка — на каком языке текст.
  3. Суммаризация — короткие выжимки.
  4. Проверка орфографии — поиск опечаток.
  5. Советы по читаемости — предложения, как сделать текст яснее.
Ключевое Ты собрал MCP-сервер на FastMCP, завернул те же функции в Gradio-UI, который сам является MCP-сервером, задеплоил на Spaces и навёл на него агента. Тот же паттерн масштабируется на интеграции с API, обработку данных и кастомную аналитику.

Юнит 2 · Квиз

Квиз 2: MCP на практике

Проверяем понимание сборки, конфигурации и деплоя MCP-систем. Восемь вопросов.

Как строить серверы на FastMCP?

Какова роль тайп-хинтов и докстрингов?

Tools, resources, prompts — что есть что?

Как включить MCP в Gradio?

Как агенты узнают про MCP-серверы?

Как задеплоить MCP-сервер на Hugging Face Spaces?

Какой транспорт когда использовать?

Как обращаться с секретами в MCP-конфигурации?

Итоги

  • 7–8 верных — ты готов строить и деплоить настоящие MCP-системы!
  • 5–6 — хорошее понимание; повтори разделы про конфигурацию и деплой.
  • 3–4 — повтори разделы про сборку и интеграцию.
  • 0–2 — пройди юнит заново с упором на ключевые концепции.

Запомнить: FastMCP — простейший путь к серверу через декораторы; тайп-хинты и докстринги — фундамент схем; Gradio с mcp_server=True даёт UI + MCP автоматом; серверы конфигурируются в конфигах агентов; stdio — локально, Streamable HTTP — удалённо; секреты — только в env-переменных; деплой на Spaces — самый простой способ поделиться.

Юнит 2 пройден! Дальше — юнит 3: плагины.

Юнит 3 · Плагины

Введение: что такое плагины

Плагины — переиспользуемые расширения для кодовых агентов. Конкретная форма зависит от платформы: Claude Code и Codex используют устанавливаемые бандлы с манифестами (плюс опциональные скиллы, MCP-серверы и интеграции с приложениями), а OpenCode — JavaScript/TypeScript-модули, загружаемые из локальных файлов или npm-пакетов.

Плагин — это готовый «пак контекста»: курированный, протестированный и расшариваемый набор знаний и инструментов, который экономит время на настройке и держит команду в едином тонусе.

Эволюция контекста

Путь к плагинам отражает взросление AI-агентов:

  1. Промпты — сырые инструкции. Хрупко, трудно переиспользовать, разнобой между людьми в команде.
  2. Скиллы — упакованные промпты + инструменты. Уже организованнее, но без управления зависимостями.
  3. MCP-серверы — стандартизированные интерфейсы к инструментам. Компонуются, но порог входа выше.
  4. Плагины — переиспользуемые расширения. В Claude Code и Codex это обычно манифест плюс собранные компоненты; в OpenCode — модуль кода, который цепляется к агенту и может добавлять поведение или инструменты.

Зачем нужны плагины

Соло-разработчику плагин экономит настройку: поставил плагин для линтинга Python или для доков к API — и получил чужую курированную работу в подарок. Команде плагины дают единицу консистентности: у всех одинаковые инструменты и инструкции. Сообществу — расшариваемый артефакт, который можно публиковать в маркетплейс.

Терминология юнита

ТерминЧто это
СкиллПереиспользуемый промпт + метаданные
MCP-серверСтандартизированный поставщик инструментов: файловый I/O, вызовы API, запросы к базам
ИнтеграцияПодключение к внешнему сервису: GitHub, Slack, Google Drive
МанифестФайл метаданных в manifest-first системах (Claude Code, Codex)
МаркетплейсКаталог, где плагины публикуются и находятся

Начнём с детального разбора анатомии плагинов.

Юнит 3 · Плагины

Анатомия плагина

Что внутри плагина — зависит от того, какая перед тобой платформа: manifest-first или code-first. Claude Code и Codex используют манифесты плюс компоненты в корне плагина. Плагины OpenCode — JavaScript/TypeScript-модули из локальных файлов или npm. Ближайший аналог у Pi — пакет, объявленный в package.json, который может включать скиллы, расширения, промпты и темы.

Универсальной формы плагина нет, но вариантов три:

  1. Manifest-first — манифест plugin.json плюс каталоги и конфиги в корне: skills/, .mcp.json, .app.json.
  2. Code-first — JS/TS-модули, экспортирующие хуки или кастомные инструменты. Скиллы и MCP-конфигурация остаются отдельными поверхностями.
  3. Package-basedpackage.json плюс конвенциональные каталоги вроде skills/ и extensions/.

Интерактив · из чего состоит manifest-first плагин (кликни по файлу)

  • 📁 my-plugin/
    • 📁 .claude-plugin/
    • 📁 skills/
    • 📁 hooks/
Выбери файл слева — здесь появится описание его роли.

Структура по платформам

Плагины Claude Code — самодостаточные каталоги. Манифест живёт в .claude-plugin/plugin.json, а скиллы, хуки, MCP-конфиг и прочие компоненты — в корне плагина.

my-plugin/
├── .claude-plugin/
│   └── plugin.json             # манифест плагина
├── skills/                     # коллекция скиллов
│   ├── analyze-text/
│   │   └── SKILL.md
│   ├── extract-keywords/
│   │   └── SKILL.md
│   └── check-reading-level/
│       └── SKILL.md
├── agents/                     # кастомные агенты (опционально)
├── .mcp.json                   # конфигурация MCP-серверов
├── hooks/                      # конфигурация хуков (опционально)
│   └── hooks.json
├── .lsp.json                   # конфигурация LSP (опционально)
├── settings.json               # дефолты плагина (опционально)
└── README.md                   # документация

plugin.json (в каталоге .claude-plugin/) объявляет плагин:

{
  "name": "text-processor-plugin",
  "version": "1.0.0",
  "description": "Скиллы текстового анализа на базе MCP-сервера text-processor",
  "author": {
    "name": "Your Name"
  }
}

Ключевые свойства: name — уникальный идентификатор, version — семантическая версия (1.0.0, 1.1.0…), description — что плагин делает, author — кто сделал.

В .claude-plugin/ лежит только plugin.json. Скиллы, агенты, хуки, MCP- и LSP-конфиги — в корне. Скиллы при вызове неймспейсятся:

/text-processor-plugin:analyze-text

MCP-серверы настраиваются в .mcp.json в корне плагина — формат тот же, что у проектного MCP-конфига из юнита 2. Сервер может быть локальным или удалённым:

{
  "mcpServers": {
    "text-processor": {
      "url": "https://YOUR-USERNAME-text-processor-mcp.hf.space/gradio_api/mcp/"
    }
  }
}

Плагины Codex тоже manifest-first. Манифест указывает на собранные скиллы, опциональный MCP-конфиг, опциональные интеграции и метаданные для установки.

my-codex-plugin/
├── .codex-plugin/
│   └── plugin.json             # манифест плагина
├── skills/                     # скиллы воркфлоу
│   ├── analyze-text/
│   │   └── SKILL.md
│   └── extract-keywords/
│       └── SKILL.md
├── .mcp.json                   # конфиг MCP-серверов (опционально)
├── .app.json                   # интеграции приложений (опционально)
├── assets/                     # иконки, скриншоты, логотипы (опционально)
└── README.md

plugin.json (в каталоге .codex-plugin/):

{
  "name": "text-processor-plugin",
  "version": "1.0.0",
  "description": "Скиллы текстового анализа на базе MCP-сервера text-processor",
  "skills": "./skills/",
  "mcpServers": "./.mcp.json",
  "apps": "./.app.json",
  "interface": {
    "displayName": "Text Processor"
  }
}

Ключевые свойства: name, version, description; skills — путь к каталогу скиллов; mcpServers — путь к MCP-конфигу (опционально); apps — путь к конфигу интеграций (опционально); interface — метаданные, которые Codex показывает при установке.

В .codex-plugin/ — только plugin.json; skills/, .mcp.json, .app.json и assets/ держи в корне.

Интеграции приложений (опционально, .app.json):

{
  "integrations": [
    {
      "name": "github",
      "type": "oauth",
      "scopes": ["repo", "user:email"]
    }
  ]
}

Установка через маркетплейсы работает двумя путями: репозиторный маркетплейс (.agents/plugins/marketplace.json в репо) и персональный (~/.agents/plugins/marketplace.json на машине пользователя). Каждая запись маркетплейса указывает на каталог плагина относительным путём с префиксом ./ в source.path плюс метаданные политики установки. Установленные копии кешируются в ~/.codex/plugins/cache/$MARKETPLACE/$PLUGIN/$VERSION/.

Совет В Codex есть скилл $skill-creator для скаффолдинга и итераций над скиллами внутри плагина — вызывай его прямо в сессии.

Плагины OpenCode — code-first. Никаких манифест-каталогов вроде .claude-plugin/ — плагин это JavaScript/TypeScript-модуль из каталога плагинов или npm.

my-opencode-project/
├── .opencode/
│   ├── plugins/
│   │   └── text-processor-plugin.ts  # локальный модуль плагина
│   └── package.json                  # опциональные зависимости
├── opencode.json                     # список npm-плагинов и конфиг агента
└── README.md

Локальный модуль плагина:

import type { Plugin } from "@opencode-ai/plugin"

export const TextProcessorPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
  await client.app.log({
    body: {
      service: "text-processor-plugin",
      level: "info",
      message: "Text Processor plugin initialized",
    },
  })

  return {
    "tool.execute.before": async (input) => {
      if (input.tool === "read") {
        await client.app.log({
          body: {
            service: "text-processor-plugin",
            level: "info",
            message: "К длинным прочитанным текстам хорошо подходят тулзы текстового анализа.",
          },
        })
      }
    },
  }
}

Фабрика плагина получает { project, client, $, directory, worktree }: через client общаешься с рантаймом OpenCode, $ — шелл-команды, directory/worktree — контекст файловой системы.

opencode.json для npm-плагинов:

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": ["@your-org/text-processor-plugin"]
}

Ключевое: локальные плагины — JS/TS-файлы в .opencode/plugins/; npm-плагины — через массив plugin в opencode.json; плагины экспортируют хуки или кастомные инструменты кодом; скиллы и MCP-серверы настраиваются отдельно от плагинов; дистрибуция — локальные файлы или npm.

Ближайший аналог плагина у Pi — Pi-пакет. Скрытого манифест-каталога нет: манифест — это package.json, а скиллы, расширения, промпты и темы Pi загружает из корня пакета.

my-pi-package/
├── package.json              # манифест пакета
├── skills/                   # скиллы
│   ├── analyze-text/
│   │   └── SKILL.md
│   └── extract-keywords/
│       └── SKILL.md
├── extensions/               # TS/JS-расширения рантайма (опционально)
│   └── text-processor.ts
├── prompts/                  # шаблоны промптов (опционально)
├── themes/                   # темы (опционально)
└── README.md

package.json:

{
  "name": "text-processor-plugin",
  "version": "1.0.0",
  "keywords": ["pi-package"],
  "pi": {
    "skills": ["./skills"],
    "extensions": ["./extensions"],
    "prompts": ["./prompts"],
    "themes": ["./themes"]
  }
}

Ключевое: манифест — package.json, каталога .pi-plugin/ не существует; скиллы в skills/ грузятся как любые другие; расширения добавляют инструменты, команды, UI и обработчики жизненного цикла; в том же пакете могут ехать промпты и темы; для MCP-инструментов пакет дополняется pi-mcp-adapter и файлом .mcp.json.

Ключевое Claude Code и Codex — manifest-first бандлы: plugin.json плюс компоненты в корне (skills/, .mcp.json, интеграции). OpenCode — code-first: плагины это JS/TS-модули из .opencode/plugins/ или npm, а скиллы и MCP настраиваются рядом. Pi-пакеты — package.json плюс конвенциональные каталоги.

Дальше собираем плагин с нуля.

Юнит 3 · Практика

Собираем свой плагин

В юните 2 у нас получился MCP-сервер text-processor. Теперь упакуем его в плагин. Для Claude Code и Codex это манифест плюс собранные скиллы и MCP-конфиг; для OpenCode — модуль плагина на TypeScript (MCP-конфигурация отдельно); для Pi — пакет с package.json, при желании в паре с pi-mcp-adapter.

Структура проекта

Создай каталог плагина рядом с уже собранным сервером:

mkdir text-processor-plugin
cd text-processor-plugin

# Структура каталогов
mkdir -p skills/{analyze-text,extract-keywords,check-reading-level}

# Основные файлы
touch README.md
touch skills/analyze-text/SKILL.md
touch skills/extract-keywords/SKILL.md
touch skills/check-reading-level/SKILL.md

Заметь отличие от юнита 2: кода сервера здесь нет. Для Claude Code и Codex плагин ссылается на уже собранный и задеплоенный MCP-сервер. Ветка OpenCode добавляет поведение кодом и может отдельно указывать на тот же сервер. Ветка Pi переиспользует тот же каталог skills/ внутри Pi-пакета и при желании дополняется адаптером MCP из юнита 2.

Шаг 1. Пишем скиллы

Этот шаг напрямую относится к Claude Code, Codex и Pi, где бандл может везти skills/. Если идёшь по ветке OpenCode — пропусти к шагу 2: плагины OpenCode — модули кода, а не наборы скиллов.

Скиллы учат агента, когда и как использовать твои MCP-инструменты. Каждый скилл оборачивает один инструмент сервера text-processor.

Скилл 1 — skills/analyze-text/SKILL.md: объясняет, что инструмент analyze_text сервера считает статистику текста. Когда применять: пользователь спрашивает про статистику, число слов/символов/предложений, среднюю длину слова или метрики читаемости. Как применять: вызвать analyze_text с полным текстом; в JSON-ответе будут total_characters, characters_without_spaces, total_words, total_sentences, average_word_length, average_sentence_length, unique_words. Пример: на вопрос «насколько сложный этот абзац?» — вызвать инструмент, интерпретировать статистику (высокая доля уникальных слов = разнообразный словарь, длинные предложения = сложная проза) и пересказать выводы человеческим языком.

Скилл 2 — skills/extract-keywords/SKILL.md: инструмент extract_keywords ищет самые важные слова. Когда: запросы про ключевые слова, термины, извлечение тем, суммаризацию контента. Как: вызвать с текстом и опциональным count (по умолчанию 5); в ответе массив keywords с полями word и frequency. Пример: «какие главные темы в статье?» — вызвать с count=10, сгруппировать связанные слова в темы, показать темы с частотами.

Скилл 3 — skills/check-reading-level/SKILL.md: инструмент check_reading_level оценивает сложность текста. Когда: вопросы про уровень чтения, сложность, «подойдёт ли новичкам». Как: вызвать с текстом; вернутся grade_level (числовой индекс Флеша-Кинкейда) и reading_level (Elementary / Middle / High School / College). Пример: «эта документация подходит новичкам?» — вызвать инструмент, сравнить уровень с целевой аудиторией, предложить упрощения, если уровень завышен.

Шаг 2. Точка входа плагина

Claude Code и Codex объявляют содержимое плагина манифестами. OpenCode — модулем плагина. Pi — package.json с опциональными extensions/.

mkdir -p .claude-plugin

.claude-plugin/plugin.json:

{
  "name": "text-processor-plugin",
  "version": "1.0.0",
  "description": "Скиллы текстового анализа на базе MCP-сервера text-processor",
  "author": {
    "name": "Your Name"
  }
}

.mcp.json указывает на твой сервер. Если используешь локальный сервер из юнита 2:

{
  "mcpServers": {
    "text-processor": {
      "command": "python",
      "args": ["../text-processor-mcp/server.py"]
    }
  }
}

Или, если задеплоил на Hugging Face Spaces:

{
  "mcpServers": {
    "text-processor": {
      "url": "https://YOUR-USERNAME-text-processor-mcp.hf.space/gradio_api/mcp/"
    }
  }
}
mkdir -p .codex-plugin

.codex-plugin/plugin.json:

{
  "name": "text-processor-plugin",
  "version": "1.0.0",
  "description": "Скиллы текстового анализа на базе MCP-сервера text-processor",
  "skills": "./skills/",
  "mcpServers": "./.mcp.json",
  "interface": {
    "displayName": "Text Processor"
  }
}

.mcp.json — для локального сервера:

{
  "mcpServers": {
    "text-processor": {
      "command": "python",
      "args": ["../text-processor-mcp/server.py"]
    }
  }
}

Или для Spaces-деплоя:

{
  "mcpServers": {
    "text-processor": {
      "url": "https://YOUR-USERNAME-text-processor-mcp.hf.space/gradio_api/mcp/"
    }
  }
}

Нативный слой плагинов OpenCode — кодовый, так что это единственная ветка курса, где документированная поверхность плагина не Python. Соберём минимальный локальный модуль и отметим, где жили бы скиллы или MCP-конфиг, если они тоже нужны.

mkdir -p .opencode/plugins

.opencode/plugins/text-processor-plugin.ts:

import type { Plugin } from "@opencode-ai/plugin"

export const TextProcessorPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
  await client.app.log({
    body: {
      service: "text-processor-plugin",
      level: "info",
      message: "Text Processor plugin initialized",
    },
  })

  return {
    "tool.execute.before": async (input) => {
      if (input.tool === "read") {
        await client.app.log({
          body: {
            service: "text-processor-plugin",
            level: "info",
            message: "После загрузки длинного документа пригодится текстовый анализ.",
          },
        })
      }
    },
  }
}

Пример намеренно минимальный: локальные плагины OpenCode — это загружаемые на старте JS/TS-модули, возвращающие хуки. Если нужны кастомные инструменты вместо хуков — хелпер tool() из @opencode-ai/plugin.

Чтобы ставить тот же плагин из npm, добавь его в opencode.json:

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": ["@your-org/text-processor-plugin"]
}

Если в OpenCode нужен и Python-сервер из юнита 2 — настрой его отдельно в opencode.json под ключом mcp. Эта MCP-конфигурация живёт рядом с системой плагинов, но самим плагином не является.

Pi-пакеты используют package.json как манифест. Переиспользуем тот же каталог skills/ и объявим его там:

mkdir -p extensions

package.json:

{
  "name": "text-processor-plugin",
  "version": "1.0.0",
  "keywords": ["pi-package"],
  "pi": {
    "skills": ["./skills"],
    "extensions": ["./extensions"]
  }
}

extensions/text-processor.ts:

import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  pi.registerCommand("text-processor-status", {
    description: "Проверить, что пакет text-processor загружен",
    handler: async (_args, ctx) => {
      ctx.ui.notify(
        "Скиллы text-processor загружены. Добавь pi-mcp-adapter, если нужны ещё и MCP-инструменты.",
      );
    },
  });
}

Каталог skills/ Pi загружает прямо из пакета. Если внутри Pi нужен сервер из юнита 2 как инструменты — добавь к пакету настройку pi-mcp-adapter из юнита 2.

Шаг 3. README для людей

На всех платформах человеческая документация живёт в README.md в корне плагина. Claude Code и Codex берут установочные метаданные из plugin.json; OpenCode загружает модуль напрямую, так что README — для разработчиков, а не для рантайма.

Что писать в README: название и назначение плагина; список скиллов (Analyze Text — статистика слов и предложений, Extract Keywords — частотные термины, Check Reading Level — оценка по Флешу-Кинкейду); как устроены версии для Claude Code/Codex (скиллы в skills/, метаданные в манифесте, опциональный .mcp.json) и для OpenCode (JS/TS-модуль в .opencode/plugins/ или npm); какие инструменты даёт связанный MCP-сервер (analyze_text, extract_keywords, check_reading_level, reverse_text); и настройку — локальный сервер (text-processor-mcp/server.py + установленный MCP-рантайм) или удалённый (URL твоего Space в .mcp.json / opencode.json).

Шаг 4. Тестируем плагин

Локальный плагин выставляется через локальный файл маркетплейса. Создай marketplace.json рядом с каталогом плагина:

{
  "name": "local-example-plugins",
  "owner": { "name": "you" },
  "plugins": [
    {
      "name": "text-processor-plugin",
      "source": "./text-processor-plugin",
      "description": "Скиллы текстового анализа"
    }
  ]
}

Затем внутри Claude Code:

/plugin marketplace add /absolute/path/to/marketplace.json
/plugin install text-processor-plugin@local-example-plugins

После установки тестируй скиллы разговорно:

Оцени уровень чтения этого текста: «Митохондрия — энергетическая
станция клетки. Она даёт энергию через окислительное
фосфорилирование».

После правок файлов плагина выключи и включи его через /plugin — так Claude Code подхватит изменения.

Скопируй плагин в персональный каталог плагинов:

mkdir -p ~/.codex/plugins
cp -R /absolute/path/to/text-processor-plugin ~/.codex/plugins/text-processor-plugin

Добавь его в персональный маркетплейс ~/.agents/plugins/marketplace.json. Поскольку source.path резолвится относительно файла маркетплейса, путь ниже сначала выходит из ~/.agents/plugins/, а потом заходит в ~/.codex/plugins/:

{
  "name": "local-example-plugins",
  "interface": {
    "displayName": "Local Example Plugins"
  },
  "plugins": [
    {
      "name": "text-processor-plugin",
      "source": {
        "source": "local",
        "path": "../../.codex/plugins/text-processor-plugin"
      },
      "policy": {
        "installation": "AVAILABLE",
        "authentication": "ON_INSTALL"
      },
      "category": "Productivity"
    }
  ]
}

Тестируй разговорно: «Проанализируй сложность этого абзаца». Открой браузер плагинов и проверь доступность:

/plugins

Установи плагин из маркетплейса, открой новый тред и проверь. После правок маркетплейса или каталога плагина — перезапусти Codex.

Положи файл плагина в .opencode/plugins/ и запусти OpenCode:

opencode

Локальные плагины загружаются на старте — плагин активен всю сессию. Тестируй любым воркфлоу с чтением длинного документа:

Суммаризируй документ и отметь, какой текстовый анализ пригодился бы дальше.

Если MCP-сервер из юнита 2 настроен отдельно в opencode.json — проверь его:

opencode mcp list

После правок файла плагина или списка plugin — перезапусти OpenCode.

Установи пакет из локального пути:

pi install /absolute/path/to/text-processor-plugin -l
pi install npm:pi-mcp-adapter

Добавь сервер из юнита 2 в .mcp.json, чтобы у скиллов пакета были инструменты:

{
  "mcpServers": {
    "text-processor": {
      "url": "https://YOUR-USERNAME-text-processor-mcp.hf.space/gradio_api/mcp/"
    }
  }
}

Запусти pi и тестируй разговорно: «Проанализируй сложность этого абзаца». После правок пакета или .mcp.json используй /reload — Pi обновит скиллы, расширения и адаптер в текущей сессии.

Финальная структура

Для Claude Code и Codex (если поддерживаешь оба) итоговый manifest-first плагин выглядит так:

text-processor-plugin/
├── .claude-plugin/
│   └── plugin.json                       # манифест Claude Code
├── .codex-plugin/
│   └── plugin.json                       # манифест Codex
├── .mcp.json                             # общий MCP-конфиг для обоих
├── README.md                             # документация
└── skills/
    ├── analyze-text/SKILL.md
    ├── extract-keywords/SKILL.md
    └── check-reading-level/SKILL.md

Для OpenCode поверхность плагина отдельная:

.opencode/
├── plugins/
│   └── text-processor-plugin.ts
└── package.json          # опционально

Обрати внимание: кода сервера в manifest-first плагине нет. MCP-сервер живёт отдельно — локально в text-processor-mcp/ из юнита 2 или на Hugging Face Spaces. Claude Code и Codex указывают на него через .mcp.json; OpenCode — отдельно в opencode.json, причём этот MCP-конфиг соседствует с плагином, а не входит в него.

Принцип Скиллы описывают, как пользоваться инструментами; MCP-серверы предоставляют инструменты; плагины упаковывают переиспользуемое поведение агента так, как ожидает конкретная платформа.

Бест-практики

Пиши описания скиллов, объясняющие когда использовать инструмент (а не только что он делает); ссылайся на существующие MCP-серверы вместо дублирования кода; держи в манифест-каталоге только plugin.json; никогда не хардкодь API-ключи (env-переменные); версионируй семантически (1.0.0, 1.1.0, 2.0.0); тестируй локально до публикации. Для OpenCode: держи модуль плагина маленьким и сфокусированным, а npm-зависимости добавляй, только когда хук или инструмент реально их требует.

Ключевое Плагины Claude Code и Codex — манифесты, связывающие скиллы и опциональные MCP/app-конфиги. Плагины OpenCode — JS/TS-модули; скиллы и MCP-серверы живут рядом. В обоих мирах сервер из юнита 2 остаётся на своём месте — плагин лишь ссылается на него или дополняет его.

Дальше — квиз, потом установка и использование опубликованных плагинов.

Юнит 3 · Квиз

Квиз 1: основы плагинов

Проверяем понимание плагинов, их анатомии и различий между платформами.

Что такое плагин?

Что делает plugin.json?

Чем manifest-first плагины различаются между платформами?

Как скиллы и MCP-серверы сочетаются внутри плагина?

На что может ссылаться плагин Codex?

Итоги

4–5 верных — анатомию плагинов ты усвоил. Промахнулся в нескольких — перечитай введение и главу про анатомию.

Запомнить: плагин — это упаковочная поверхность, не то же самое, что скилл или MCP-сервер; Claude Code и Codex — manifest-first, OpenCode — code-first; на manifest-first платформах plugin.json говорит агенту, где лежат компоненты.

Дальше — плагины в реальной работе: установка и активация на каждой платформе.

Юнит 3 · Плагины

Используем плагины

Плагин собран — теперь учимся ставить и использовать плагины, свои и чужие. Процесс слегка отличается по платформам, но ядро одно: найти → установить → настроить → пользоваться.

Установка плагинов

Claude Code ставит плагины через маркетплейсы и команду /plugin в сессии.

Из маркетплейса:

/plugin marketplace add <owner>/<repo>
/plugin install <plugin-name>@<marketplace-name>

/plugin открывает браузер плагинов: включение, выключение, удаление — интерактивно.

Из локального каталога: для локальной разработки наведи /plugin на файл marketplace.json, ссылающийся на каталог твоего плагина:

{
  "name": "local-example-plugins",
  "owner": { "name": "you" },
  "plugins": [
    {
      "name": "text-processor-plugin",
      "source": "./text-processor-plugin",
      "description": "Скиллы текстового анализа"
    }
  ]
}
/plugin marketplace add /absolute/path/to/marketplace.json
/plugin install text-processor-plugin@local-example-plugins

Claude Code читает .claude-plugin/plugin.json, загружает скиллы из skills/ и запускает MCP-серверы из .mcp.json.

Скиллы плагина доступны разговорно («Оцени уровень чтения этого абзаца») или по неймспейсному имени:

/text-processor-plugin:check-reading-level

Управление: /plugin — просмотр, включение/выключение, удаление. После правок локального плагина выключи и включи его там же, чтобы перезагрузить.

Конфигурация: переменные окружения, нужные плагинам, задаются в шелле до запуска:

export HF_TOKEN="hf_xxxxx"

Codex ставит плагины из встроенного браузера плагинов или из маркетплейс-файлов (для локальных и неопубликованных).

Из браузера плагинов: открой каталог в CLI, найди, установи, открой новый тред:

/plugins

Из персонального маркетплейса: скопируй плагин в персональный каталог:

mkdir -p ~/.codex/plugins
cp -R /local/path/to/text-processor-plugin ~/.codex/plugins/text-processor-plugin

И пропиши его в ~/.agents/plugins/marketplace.json (путь source.path резолвится относительно этого файла, поэтому он сначала поднимается из ~/.agents/plugins/):

{
  "name": "local-example-plugins",
  "interface": {
    "displayName": "Local Example Plugins"
  },
  "plugins": [
    {
      "name": "text-processor-plugin",
      "source": {
        "source": "local",
        "path": "../../.codex/plugins/text-processor-plugin"
      },
      "policy": {
        "installation": "AVAILABLE",
        "authentication": "ON_INSTALL"
      },
      "category": "Productivity"
    }
  ]
}

Codex: читает .codex-plugin/plugin.json → загружает скиллы из skills/ → поднимает опциональные MCP-серверы из .mcp.json → кеширует плагин в ~/.codex/plugins/cache/$MARKETPLACE/$PLUGIN/$VERSION/.

Из репозиторного маркетплейса: создай $REPO_ROOT/.agents/plugins/marketplace.json в репо, которое должно раздавать список плагинов, держи каталоги плагинов по репо-относительным путям вроде ./plugins/text-processor-plugin, перезапусти Codex и открой /plugins в этом репо.

Использование: спрашивай естественно — «Какой уровень чтения у этого README?» — Codex распознает задачу текстового анализа, найдёт скиллы плагина, вызовет нужный с параметрами и вернёт результат. Явно выбрать плагин или скилл — через @:

@text-processor-plugin

Управление: /plugins — инспекция установленных. После правок маркетплейса, каталога плагина или флага enabled в ~/.codex/config.toml — перезапусти Codex.

Конфигурация: env-переменные в шелле (export HF_TOKEN="hf_xxxxx"); MCP-серверы стартуют, если в .mcp.json есть записи mcpServers.

Плагины OpenCode — JS/TS-модули, загружаемые на старте. Скиллы и MCP-конфигурация — смежные поверхности, но не сам плагин.

Из локальных файлов — положи .js/.ts в один из каталогов плагинов, они загрузятся автоматически при старте:

.opencode/plugins/              # плагины уровня проекта
~/.config/opencode/plugins/     # глобальные плагины

Из npm — добавь имена пакетов в opencode.json; OpenCode ставит их через Bun при старте:

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": ["@your-org/text-processor-plugin"]
}

Что остаётся отдельным: переиспользуемые скиллы кладутся в .opencode/skills/ (OpenCode также видит .claude/skills/ и .agents/skills/), а MCP-серверы настраиваются в opencode.json:

{
  "mcp": {
    "text-processor": {
      "type": "remote",
      "url": "https://YOUR-USERNAME-text-processor-mcp.hf.space/gradio_api/mcp/"
    }
  }
}

Использование: после старта хуки и кастомные инструменты плагина активны. Хуки срабатывают на своих событиях; кастомные инструменты OpenCode вызывает наравне со встроенными.

Управление: удалить файл из .opencode/plugins/ или убрать пакет из массива plugin. После правок — перезапуск OpenCode.

Аналог плагина у Pi — Pi-пакет: локальная папка, git-репозиторий или npm-пакет со скиллами, расширениями, промптами и темами.

Из локального каталога (-l записывает пакет в .pi/settings.json текущего проекта):

pi install /local/path/to/text-processor-plugin -l

Из npm или git:

pi install npm:@your-org/text-processor-plugin
pi install git:github.com/your-org/text-processor-plugin

В паре с MCP: если скиллы пакета зависят от сервера из юнита 2 — поставь адаптер и добавь сервер в .mcp.json:

pi install npm:pi-mcp-adapter
{
  "mcpServers": {
    "text-processor": {
      "url": "https://YOUR-USERNAME-text-processor-mcp.hf.space/gradio_api/mcp/"
    }
  }
}

Использование — разговорно или явно:

Какой уровень чтения у этого README?
/skill:check-reading-level Какой уровень чтения у этого README?

Управление:

pi list
pi config
pi remove /local/path/to/text-processor-plugin

После правок локальных файлов пакета или .mcp.json/reload внутри Pi.

Типовые задачи

Настройка API-ключей

Всем платформам нужны API-ключи для плагинов, которые ходят во внешние сервисы. Общий паттерн — переменные окружения в шелле до запуска агента:

export HF_TOKEN="hf_xxxxx"
export GITHUB_TOKEN="ghp_xxxxx"

Claude Code и Codex читают их при загрузке плагинов, без настройки внутри приложения. OpenCode дополнительно позволяет задать их в opencode.json в блоке environment каждого MCP-сервера. Pi читает шелл-окружение напрямую, а pi-mcp-adapter умеет интерполировать переменные в записях .mcp.json.

Плагин не загружается?

Проверь, что манифест существует и валиден, и что каталог скиллов на месте:

cat ./my-plugin/.claude-plugin/plugin.json
ls -la ./my-plugin/skills/

Затем перевключи плагин через /plugin.

Проверь, что плагин есть в маркетплейсе и оформлен правильно:

cat ~/.agents/plugins/marketplace.json

Сверь пути skills и mcpServers в .codex-plugin/plugin.json:

{
  "skills": "./skills/",
  "mcpServers": "./.mcp.json"
}

Открой /plugins. Если менял маркетплейс или конфиги — сначала перезапусти Codex.

Проверь, что файл плагина лежит в поддерживаемом каталоге:

ls -la .opencode/plugins/

Для npm-плагинов сверь массив plugin в opencode.json. Если плагин импортирует внешние пакеты — они должны быть в .opencode/package.json. Потом перезапусти OpenCode.

Посмотри список установленных пакетов:

pi list

После исправления локальных файлов — /reload, чтобы Pi обновил пакет в текущей сессии.

MCP-сервер плагина не стартует?

Проверь, что .mcp.json — валидный JSON с ключом mcpServers:

cat ./.mcp.json

После правки конфига перевключи плагин через /plugin.

MCP-серверы в Codex-плагинах опциональны. Если не стартуют, проверь: .codex-plugin/plugin.json указывает mcpServers на ./.mcp.json; в .mcp.json валидный объект mcpServers; запись маркетплейса смотрит на правильный каталог плагина.

Плагины и MCP-серверы в OpenCode раздельны. Если отдельно настроенный сервер не стартует — дебажь MCP-конфиг:

opencode mcp debug text-processor

Проверь, что opencode.json — валидный JSON, а команды серверов указывают на существующие файлы.

Pi-пакеты и MCP раздельны. Если пакет загрузился, а адаптер сервер не видит:

cat ./.mcp.json

Внутри Pi:

/mcp
/mcp reconnect text-processor
/mcp tools

Проверь, что pi-mcp-adapter установлен, а запись сервера содержит валидные command/args или url.

Ключевое Claude Code: /plugin marketplace add + /plugin install, браузер — /plugin. Codex: /plugins плюс репозиторные и персональные маркетплейс-файлы, после правок — перезапуск. OpenCode: нативные плагины — JS/TS-модули в .opencode/plugins/ или npm-пакеты из opencode.json; скиллы и MCP — отдельные точки расширения. Pi: pi install, ресурсы в package.json + skills//extensions/, для MCP — pi-mcp-adapter.

Дальше — квиз про дистрибуцию плагинов и бест-практики.

Юнит 3 · Квиз

Квиз 2: сборка и дистрибуция плагинов

Проверяем понимание разработки, дистрибуции и бест-практик плагинов.

Где живёт plugin.json и что он делает?

Как выглядит структура manifest-first плагина?

Как проектировать права (permissions) плагина?

Как тестировать плагины локально?

Как обращаться с секретами?

Итоги юнита 3

4–5 верных — ты готов собирать и распространять плагины ответственно. Если нет — перечитай главы про сборку и использование до публикации чего-либо.

Запомнить: держи структуру плагина явной — у манифеста, компонентов и документации своё чёткое место; проси минимум прав и объясняй зачем; тестируй локально до публикации; секреты — в env-переменных, а не в файлах плагина.

Юнит 3 пройден! Ты умеешь упаковывать переиспользуемое поведение агента и безопасно его распространять. Дальше — юнит 4, где эти компоненты станут кирпичами мультиагентных воркфлоу.

Юнит 4 · Сабагенты

Введение: зачем сабагенты

Сабагенты — изолированные экземпляры агентов, которых родительский агент запускает под подзадачи, часто параллельно. У каждого — своё контекстное окно, свои лимиты выполнения и свой доступ к инструментам; результаты он возвращает родителю.

Родительский агент (основная задача)
    │
    ├─→ Сабагент 1 (подзадача A) — свой контекст, свои инструменты
    ├─→ Сабагент 2 (подзадача B) — свой контекст, свои инструменты
    └─→ Сабагент 3 (подзадача C) — свой контекст, свои инструменты

    Дождаться всех → Агрегировать результаты → Финальный ответ

Где одиночный агент упирается в потолок

Один агент раз за разом натыкается на одни и те же проблемы. Крупные задачи переполняют его контекстное окно. Работа идёт последовательно даже там, где могла бы идти параллельно. Агент, от которого ждут экспертизы во всём сразу, размывает фокус. Несвязанные задачи мешают друг другу в одной цепочке рассуждений. И один сбой валит весь воркфлоу — изоляции-то нет. Сабагенты закрывают все эти проблемы.

Пять сценариев для сабагентов

Есть пять чётких сигналов, что сабагенты — правильный выбор.

1. Ресёрчевые задачи. Когда нужно прочитать 10+ файлов или документов:

Родитель: «Суммаризируй нашу архитектуру»
├─ Сабагент A: читает и сжимает доки бэкенда (50 файлов)
├─ Сабагент B: читает и сжимает доки фронтенда (40 файлов)
└─ Сабагент C: читает и сжимает доки базы данных (30 файлов)
Родитель: собирает выжимки в обзор архитектуры

2. Несколько независимых задач. Когда есть 3+ куска работы, не зависящих друг от друга:

Родитель: «Подготовь отчёт к запуску»
├─ Сабагент A: собирает метрики продаж (2 часа API-вызовов)
├─ Сабагент B: компилирует список фич из GitHub (1 час)
└─ Сабагент C: собирает фидбек пользователей из опросов (1 час)
Родитель: сводит всё в один отчёт

Один агент потратил бы 4 часа. Сабагенты — около 2 (за счёт параллельности).

3. Проверка свежим взглядом. Когда нужно непредвзятое ревью или независимая верификация:

Родитель: «Реализуй платёжную систему»
├─ Сабагент A: реализует фичу
└─ Сабагент B (read-only): ревьюит реализацию на баги

Проверяющий сабагент не помнит, какие решения принимались по ходу реализации, — поэтому он и замечает дыры.

4. Проверка перед коммитом. Независимая валидация до мержа:

Родитель: «Предложи изменения в коде»
├─ Сабагент A: пишет код
└─ Сабагент B (read-only): security-ревью перед коммитом

Плохой код просто не доезжает до репозитория.

5. Пайплайн-воркфлоу. Когда у задачи чёткие последовательные стадии:

Родитель оркестрирует:
├─ Стадия дизайна
│  └─ Сабагент: спроектировать схему API
├─ Стадия реализации
│  └─ Сабагент: написать код (по дизайну из стадии 1)
└─ Стадия тестирования
   └─ Сабагент: протестировать код (из стадии 2)

Чистое разделение ответственности, прогресс легко отслеживать. Один агент справился бы с этими стадиями и последовательно, но сабагенты дают каждой стадии изолированное контекстное окно: тестирующий сабагент получает только готовый код — без всей истории дизайн-дискуссий и решений, накопившихся в контексте родителя. Каждая стадия остаётся сфокусированной, раздувание контекста не деградирует качество на длинных пайплайнах. Плюс сабагент может быть придирчивее и критичнее к работе предыдущего агента — итоговые результаты выходят крепче.

Сильный сигнал: «10+ файлов»

Верный признак, что пора доставать сабагентов, — если ловишь себя на мысли:

  • «Чтобы в этом разобраться, мне надо прочитать 10+ файлов»;
  • «Я делаю 3+ независимых куска работы»;
  • «Хочу второе мнение по этому поводу».

Что дают сабагенты

Параллельное выполнение нескольких задач одновременно; отдельное контекстное окно на задачу (никакого переполнения); специализированный доступ к инструментам на агента; изоляция сбоев — один упавший не валит весь воркфлоу; масштабирование без замедления родителя; и естественная декомпозиция сложности на управляемые куски.

Когда сабагенты НЕ нужны

Избегай сабагентов для: последовательной зависимой работы, где задаче B нужен результат задачи A; параллельных правок одного файла (это гарантированные git-конфликты); маленьких быстрых задач, где накладные расходы на запуск больше самой задачи; и воркфлоу, требующих больше пяти агентов-специалистов, — там координация превращается в хаос.

Что будет в юните

Паттерны сабагентов (fan-out/fan-in, пайплайн, супервизор, рой), сигналы, что задаче нужны сабагенты, как их вызывать в Claude Code и Codex, и hands-on: пайплайн «ресёрч → реализация → ревью».

Юнит 4 · Сабагенты

Паттерны сабагентов

Мультиагентные воркфлоу сводятся к горстке форм. Разберём четыре, которых хватит почти на всё, что ты будешь строить.

Интерактив · четыре паттерна (листай)

Fan-Out/Fan-In
Пайплайн
Супервизор
Рой
1/4

Паттерн 1: Fan-Out / Fan-In

Что это: запустить несколько сабагентов параллельно под независимые подзадачи и агрегировать результаты. Когда: задачи независимы, зависимостей нет. Пример: прогнать 5 моделей на одном бенчмарке.

Родитель: «Сравни модели»
├─ Сабагент 1: оценивает модель A → 92%
├─ Сабагент 2: оценивает модель B → 89%
├─ Сабагент 3: оценивает модель C → 94%
├─ Сабагент 4: оценивает модель D → 88%
└─ Сабагент 5: оценивает модель E → 91%

Родитель: агрегирует → отчёт с рейтингом

Псевдокод:

parent_agent:
    subagents = []
    for model in [A, B, C, D, E]:
        subagent = spawn(
            task=f"Оцени {model}",
            context={model}
        )
        subagents.append(subagent)

    results = wait_all(subagents)
    report = aggregate(results)
    return report

Плюсы: полная параллельность, максимальная скорость, легко масштабировать. Минусы: общее время = самый медленный сабагент.

Паттерн 2: Пайплайн

Что это: цепочка сабагентов, где выход одного становится входом следующего. Когда: последовательная обработка с потоком данных через стадии. Пример: обработка данных: извлечь → почистить → проанализировать → отчитаться.

Сырые данные
  ↓
[Сабагент 1: Извлечение]
  ↓
[Сабагент 2: Очистка]
  ↓
[Сабагент 3: Анализ]
  ↓
[Сабагент 4: Отчёт]
  ↓
Финальный отчёт

Псевдокод:

parent_agent:
    data = load_raw_data()

    extracted = spawn(task="Извлечь", input=data).wait()
    cleaned = spawn(task="Почистить", input=extracted).wait()
    analyzed = spawn(task="Проанализировать", input=cleaned).wait()
    report = spawn(task="Отчёт", input=analyzed).wait()

    return report

Плюсы: чёткие стадии, прогресс легко отслеживать, сбои локальны. Минусы: параллельности нет — каждая стадия ждёт предыдущую.

Паттерн 3: Супервизор (иерархический)

Что это: родитель управляет несколькими специализированными сабагентами, у каждого свои инструменты и экспертиза. Когда: сложные задачи, требующие нескольких специалистов. Пример: отчёт о запуске продукта из данных продаж, инжиниринга и маркетинга.

Родительский агент (оркестратор)
├─ Сабагент продаж (инструменты: CRM, метрики выручки)
├─ Сабагент инжиниринга (инструменты: GitHub API, деплой)
└─ Сабагент маркетинга (инструменты: аналитика, соцсети)

Родитель агрегирует → отчёт о запуске

Псевдокод:

parent_agent:
    sales = spawn(
        task="Собери метрики продаж",
        tools=[crm, revenue_db]
    )
    engineering = spawn(
        task="Собери статус фич",
        tools=[github, ci_cd]
    )
    marketing = spawn(
        task="Собери настроение рынка",
        tools=[analytics, social]
    )

    results = wait_all([sales, engineering, marketing])
    report = combine(results)
    return report

Плюсы: специализированные агенты, чистое разделение инструментов, параллельность. Минусы: координация сложнее.

Паттерн 4: Рой (коллаборативный)

Что это: несколько сабагентов работают над одной проблемой, сравнивают подходы и сходятся к лучшему решению. Когда: сложные проблемы, где качество растёт от количества перспектив. Пример: ревью дизайна силами архитектора, безопасника и перформанс-инженера.

Родитель: «Спроектируй новый API»
├─ Сабагент 1 (архитектор): ревью структуры
├─ Сабагент 2 (безопасность): поиск уязвимостей
├─ Сабагент 3 (производительность): поиск узких мест

Родитель: учитывает фидбек → финальный дизайн

Псевдокод:

parent_agent:
    design = "Первоначальный дизайн API..."

    # Раунд 1: независимые ревью
    architect_review = spawn(task="Ревью как архитектор", input=design).wait()
    security_review = spawn(task="Ревью безопасности", input=design).wait()
    perf_review = spawn(task="Ревью производительности", input=design).wait()

    # Учитываем фидбек
    improved_design = combine(design, architect_review,
                              security_review, perf_review)

    return improved_design

Плюсы: несколько перспектив повышают качество, ловятся слепые зоны. Минусы: может быть медленно (нужно несколько раундов).

Сравнение паттернов

ПаттернСценарийПараллельностьКоординация
Fan-Out/Fan-InНезависимые задачиВысокаяНизкая
ПайплайнПоследовательные стадииНетСредняя
СупервизорНесколько специалистовСредняя-высокаяСредняя-высокая
РойКоллаборативный дизайнНизкая-средняяВысокая

Антипаттерны: когда сабагенты не нужны

Общие принципы были во введении — вот как они выглядят в коде.

1. Последовательная зависимая работа:

# ТАК НЕ НАДО
subagent_a = spawn(task="Шаг 1: настроить окружение").wait()
subagent_b = spawn(task="Шаг 2: задеплоить код", input=subagent_a).wait()

# ЛУЧШЕ: один агент на последовательную работу
single_agent(task="Настрой окружение, потом задеплой")

2. Параллельные правки одного файла:

# ТАК НЕ НАДО
subagent_a = spawn(task="Добавь фичу A в app.py")
subagent_b = spawn(task="Добавь фичу B в app.py")  # Конфликт!

# ЛУЧШЕ: fan-out по разным файлам
subagent_a = spawn(task="Напиши фичу A в feature_a.py")
subagent_b = spawn(task="Напиши фичу B в feature_b.py")

3. Маленькие быстрые задачи:

# ТАК НЕ НАДО для простых задач
subagent = spawn(task="Отформатируй этот JSON")  # оверхед 1-2 c, задача 0.1 c

# ЛУЧШЕ: сделать в родителе
formatted = json.dumps(data)

4. Слишком много специалистов:

# ТАК НЕ НАДО
for i in range(20):  # 20 агентов-специалистов
    subagent = spawn(task=f"Задача специалиста {i}")

# ЛУЧШЕ: сгруппировать специалистов
senior_architect = spawn(task="Веди дизайн архитектуры")
implementation_team = spawn(task="Реализуй по дизайну")
Ключевое Fan-out/fan-in параллелит независимую работу, пайплайн сцепляет стадии, супервизор координирует специалистов, рой кросс-ревьюит один артефакт. Доставай сабагентов при куче файлов или нескольких независимых задачах; для мелочей и тесно связанных воркфлоу оставайся на одном агенте.

Дальше — как реально вызывать сабагентов в Claude Code и Codex.

Юнит 4 · Сабагенты

Используем сабагентов

Подходы у Claude Code, Codex и Pi разные, но идея одна: родительский агент делегирует очерченную задачу дочернему, часто параллельно.

Вызов сабагентов

Разговорный вызов

Самый простой способ — просто попросить:

Мне нужно сравнить 5 ML-фреймворков. Используй сабагентов,
чтобы ресёрчить каждый параллельно.
По каждому фреймворку найди:
  - метрики GitHub (звёзды, контрибьюторы)
  - популярность скачиваний (PyPI, npm)
  - настроение комьюнити (обсуждения на GitHub)

Claude Code сам распознаёт работу, подходящую для сабагентов: запускает 5 параллельных сабагентов → каждый ресёрчит свой фреймворк → результаты сводятся в сравнительный отчёт. Вся логика — за кадром.

Кастомные агенты

Для большего контроля агенты определяются в .claude/agents/*.md с YAML-фронтматтером:

---
name: researcher
description: Ресёрч-агент для глубокого исследования файлов
tools: Read, Grep, Glob, WebFetch
model: sonnet
---
Ты — эксперт-исследователь. Твоя работа — тщательно исследовать
файлы, извлекать ключевую информацию и давать выводы с источниками.

Явный вызов:

Используй сабагента researcher: исследуй нашу систему аутентификации
и отчитайся о практиках безопасности.

Claude Code запустит сабагента researcher с указанными инструментами и системным промптом. Кастомных агентов может быть сколько угодно — например, architect (дизайн систем, tools: Read, Glob — «думаешь о масштабируемости, надёжности и поддерживаемости») и security-reviewer (ревью безопасности, tools: Read, Grep — «ты эксперт по безопасности, ищи уязвимости»). И дальше:

Пусть architect отревьюит систему, а security-reviewer проверит уязвимости.

Политики в CLAUDE.md

В .claude/CLAUDE.md можно записать переиспользуемые правила, когда применять сабагентов, — например: при изменениях кода в auth/ или security/ автоматически запускать read-only сабагента security-reviewer (Read и Grep, без Write), ревьюить уязвимости и докладывать до коммита; ресёрч-задачи (10+ файлов) всегда вести сабагентами, по одному на подсистему, чтобы не переполнять контекст.

Claude Code использует эти политики как ориентир. Они делают делегирование консистентнее, но явная просьба — по-прежнему самый сильный сигнал, когда нужен конкретный сабагент.

Скиллы в сабагентах

Скиллы можно вызывать прямо в задачах сабагентов:

Используй сабагента, чтобы прогнать скилл «hf-api-search»
и найти все BERT-модели

Или закрепить скиллы за агентом:

---
name: hf-researcher
description: Специалист по Hugging Face
skills:
  - hf-api-search
  - hf-dataset-fetch
model: sonnet
---
Ты — эксперт по поиску и оценке моделей на Hugging Face.

Хуки и события жизненного цикла

Claude Code поддерживает хуки через формат hooks.json. Хуки могут запускать команды на событиях вроде PostToolUse, но автоматический запуск сабагентов из хуков — не встроенная фича. Вместо этого используй политики CLAUDE.md (см. выше): например, «при изменениях в auth/ — read-only security-reviewer с проверкой на SQL-инъекции, утечки токенов и эскалацию привилегий» или «большие тестовые прогоны (10+ файлов) — worker-сабагент с отчётом о покрытии». Это гибче жёстких хуков, но это ориентир, а не гарантированный триггер: нужен конкретный сабагент — попроси явно.

Фоновое выполнение (Ctrl+B)

Пока задача сабагента выполняется, нажми Ctrl+B — она уйдёт в фон, а ты продолжишь работать. Список активных фоновых задач — команда /tasks; оттуда же задачу можно вернуть на передний план.

Результаты сабагентов

Сабагенты возвращают: структурированные находки, файлы-источники с номерами строк, уровни уверенности и рекомендации родителю. Родитель на их основе комбинирует находки, принимает решения, генерирует итог и разруливает конфликты и противоречия.

Пример: пайплайн «ресёрч + ревью»

Задача: «Спроектируй новую систему аутентификации для приложения»

Claude Code автоматически:
1. [Запускает сабагента researcher]
   Задача: «Исследуй текущую auth-систему в 15+ файлах»
   Инструменты: Read, Grep, Glob
   Результат: текущая архитектура, болевые точки

2. [Запускает сабагента architect]
   Задача: «Спроектируй новую auth по ресёрчу из шага 1»
   Инструменты: Read (без Write)
   Результат: предлагаемый дизайн

3. [Запускает сабагента security-reviewer]
   Задача: «Отревьюй дизайн на проблемы безопасности»
   Инструменты: Read (read-only)
   Результат: отчёт об уязвимостях, рекомендации

4. [Родитель комбинирует]
   Вход: ресёрч, дизайн, security-ревью
   Выход: финальное предложение дизайна с отмеченными рисками

Бест-практики в Claude Code

Начинай с разговорного вызова — пусть Claude сам решает, когда сабагенты помогут. Определяй кастомных агентов под роли специалистов (security-reviewer, performance-tester). Ревью-агентам давай read-only инструменты — защита от случайных записей. Используй политики CLAUDE.md для единых правил в команде, следи за фоновыми задачами через /tasks, а конфликты между сабагентами оставляй разруливать родителю.

Запуск сабагентов

Codex создаёт сабагентов только по явной просьбе на естественном языке. Отдельного бинарника codex-agent или CLI-команд нет.

Запусти по одному агенту на каждый пункт этого списка.
Пусть pr_explorer замапит затронутые пути в коде.
Используй дефолтного агента для ресёрча API.

Codex распознаёт просьбу и сам запускает подходящих агентов.

Встроенные типы агентов

У Codex 3 дефолтных типа: default — универсальный запасной; worker — заточен на исполнение, реализацию и фиксы; explorer — «читающий», оптимизирован под исследование кодовой базы.

Глобальная конфигурация

Поведение сабагентов настраивается в конфиге Codex в секции [agents]:

НастройкаДефолтЗачем
max_threads6Максимум одновременных открытых тредов агентов
max_depth1Максимальная глубина вложенности агентов
job_max_runtime_seconds1800Таймаут на воркера в батч-задачах
[agents]
max_threads = 8
max_depth = 1
job_max_runtime_seconds = 3600

Управление сабагентами

Команда /agent — список активных тредов и переключение между ними. Подключившись к треду, рули им естественным языком:

Скажи этому агенту проверить ещё и безопасность.
Останови текущего агента.
Закрой этого агента и доложи находки.

Кастомные типы агентов

Определяются TOML-файлами в ~/.codex/agents/ (пользователь) или .codex/agents/ (проект):

# .codex/agents/pr_explorer.toml
name = "pr_explorer"
description = "Мапит кодовую базу и собирает доказательства для ревью PR"
developer_instructions = """
Ты — read-only исследователь кодовой базы. Мапь пути кода,
затронутые PR, находи зависимости и собирай факты о том,
что и почему изменилось.
"""

nickname_candidates = ["Atlas", "Delta", "Echo"]
sandbox_mode = "read-only"
# .codex/agents/docs_researcher.toml
name = "docs_researcher"
description = "Проверяет API и поведение фреймворков через MCP-сервер документации"
developer_instructions = """
Ты проверяешь, что код использует API правильно. Сверяйся с
документацией, ищи депрекейты и проверяй описанное в коде
поведение фреймворка.
"""

sandbox_mode = "read-only"
mcp_servers = ["mcp://docs.framework.org"]

Обязательные поля: name, description, developer_instructions (системный промпт). Опциональные: nickname_candidates, model, sandbox_mode (read-only для безопасных ревьюеров), mcp_servers, skills.config.

Наследование песочницы и одобрений

Сабагенты наследуют конфигурацию песочницы родительской сессии:

Родитель: «Отревьюй код на безопасность. Запусти security-reviewer».
→ security-reviewer автоматически наследует read-only режим родителя

Родитель: «Найди и почини все баги» (с флагом --yolo)
→ worker-агенты наследуют полные права

Запросы на одобрение из неактивных тредов всплывают к родителю. Переопределения родительской сессии (режим песочницы, --yolo) заново применяются к детям.

Батч-обработка CSV

CSV-файлы обрабатываются параллельными worker-агентами:

Обработай этот CSV по одному агенту на строку.
Колонку "model_name" нужно оценить на "benchmark".
Сохрани результаты в output.csv.

Codex использует инструмент spawn_agents_on_csv с параметрами: csv_path (входной CSV), instruction (шаблон задачи с плейсхолдерами {column}), id_column (уникальный идентификатор строки), output_schema (структура результата), output_csv_path (куда писать сводные результаты), max_concurrency (сколько агентов параллельно), max_runtime_seconds (таймаут на воркера). Каждый воркер обязан ровно один раз вызвать report_agent_job_result со структурированным результатом.

Пример: ревью PR специализированными агентами

Определи трёх кастомных агентов: pr_explorer (read-only, мапит код и собирает факты), reviewer (read-only, проверяет корректность, безопасность, покрытие тестами), docs_researcher (read-only, проверяет API через MCP-сервер документации). И вызывай естественно:

Пусть pr_explorer замапит затронутые пути в коде,
reviewer найдёт реальные риски и проверит полноту тестов,
а docs_researcher проверит корректность использования API фреймворков.

Codex запустит всех троих параллельно; в результатах — затронутые файлы с номерами строк, проблемы безопасности и корректности, пробелы в тестах, верификация API и рекомендации.

Пример: батч-оценка моделей

Мне нужно оценить 5 моделей на 3 бенчмарках (15 задач).
Обработай CSV по одному воркеру на строку.
Колонка "model" — имя модели, колонка "benchmark" — бенчмарк.
Задача: «Загрузи модель {model}, прогони на {benchmark},
доложи F1 и латентность».
Вывод в results.csv.
Максимум 5 воркеров параллельно, таймаут 30 минут на задачу.

Codex: читает CSV с 15 строками → запускает до 5 воркеров → каждый оценивает свою пару модель-бенчмарк → результаты пишутся в results.csv → родитель агрегирует и анализирует.

Бест-практики в Codex

Проси сабагентов разговорно («Запусти агентов…», «Пусть агент X заресёрчит…»); ревьюерам — read-only песочницу; специалистов оформляй кастомными агентами (pr_explorer, security-reviewer, docs_researcher); следи через /agent; для параллельной работы — CSV-батчи (оценка, тесты, ресёрч); песочницу дети наследуют сами — повторять настройки не нужно.

В этом издании курса пошаговый воркфлоу сабагентов разобран для Claude Code, Codex и Pi. В OpenCode те же идеи реализуются через его систему плагинов и хуков (см. юнит 3): паттерны — fan-out/fan-in, пайплайн, супервизор, рой — платформо-независимы.

Установка расширения сабагентов

В Pi встроенных сабагентов нет, но в официальном репозитории есть пример расширения subagent, которое запускает изолированные pi-сабпроцессы. Локальная настройка проекта:

mkdir -p .pi/extensions/subagent .pi/agents .pi/prompts

Кастомные агенты

Определяются в .pi/agents/*.md с YAML-фронтматтером:

---
name: researcher
description: Ресёрч-агент для глубокого исследования файлов
tools: read, grep, find, ls
---
Ты — эксперт-исследователь. Исследуй файлы широко, сжимай находки
и возвращай только тот контекст, который понадобится другому агенту.

Промпты-воркфлоу

Расширение поддерживает переиспользуемые шаблоны промптов в .pi/prompts/:

/implement добавь OAuth2-аутентификацию
/scout-and-plan отрефактори auth под OAuth

Разговорный вызов

С загруженным расширением можно просить и естественно:

Используй scout, чтобы замапить auth-систему, потом пусть planner
составит план реализации.
После изменения кода прогони двух ревьюеров параллельно.

Расширение делегирует каждую подзадачу отдельному pi-процессу со своим контекстным окном.

Бест-практики в Pi

Держи агентов в .pi/agents/, ревьюерам — read-only списки инструментов, оркестрацию строй на шаблонах промптов или явных просьбах. Поскольку всё это на расширениях, а не встроено — точный воркфлоу настраивается под себя.

Изоляция через worktree (Claude Code)

Когда сабагенты параллельно правят код — используй git worktree, чтобы избежать конфликтов:

# Основное рабочее пространство
git worktree add ../feature-a
git worktree add ../feature-b

# Сабагент A работает в feature-a/
# Сабагент B работает в feature-b/
# Конфликтов нет — каталоги раздельные

Скажи сабагентам, где работать:

Сабагент A: делай фичу A в каталоге ../feature-a/
Сабагент B: делай фичу B в каталоге ../feature-b/
Потом родитель мержит обе фичи

Пример из жизни: пайплайн код-ревью

Задача: «Отревьюй предлагаемую платёжную систему на проблемы безопасности и производительности».

Главный агент:
  Задача: ревью предложения платёжной системы

  1. Запустить сабагента security-reviewer (read-only)
     Инструменты: Read, Grep
     Задача: найти уязвимости

  2. Запустить сабагента performance-reviewer (read-only)
     Инструменты: Read, Grep
     Задача: найти узкие места

  3. [Оба работают параллельно]

  4. Главный агент сводит находки
     Выход: финальное ревью с рисками

Разговорно:

Используй сабагентов для ревью безопасности и производительности
платёжной системы.
Пусть security-reviewer ищет SQL-инъекции, утечки токенов и т.п.
Пусть performance-reviewer ищет тяжёлые запросы к базе и
возможности кеширования.
Запусти обоих параллельно.
Запусти двух сабагентов на ревью платёжной системы:

Агент 1 (security-reviewer):
  Задача: найти уязвимости (SQL-инъекции, утечки токенов,
  эскалация привилегий)

Агент 2 (performance-reviewer):
  Задача: найти узкие места (медленные запросы, N+1,
  возможности кеширования)

Запусти обоих параллельно.
Сведи находки и доложи риски с рекомендациями.

Codex запустит обоих ревьюеров, они параллельно исследуют payment_system/, и родитель сведёт находки в финальный отчёт. Через /agent можно следить за прогрессом или рулить любым из них.

Смоделируй то же самое как две параллельные задачи с read-only доступом (ревью безопасности + ревью производительности) и попроси агента свести риски и конкретные фиксы в один отчёт.

Используй расширение subagent для параллельного ревью платёжной системы:

Задача 1 (ревьюер безопасности):
  Найди SQL-инъекции, утечки токенов, эскалацию привилегий
  и небезопасную работу с секретами.

Задача 2 (ревьюер производительности):
  Найди медленные запросы, N+1-паттерны, лишнюю работу в
  request-путях и отсутствующие кеши.

Запусти обе параллельно, потом суммаризируй риски и конкретные фиксы.

С официальным примером расширения это обычно моделируется как параллельные задачи, каждая нацеленная на именованного агента из .pi/agents/.

Ключевое Claude Code: разговорный вызов, кастомные агенты в .claude/agents/, политики CLAUDE.md, фон по Ctrl+B. Codex: запрос на естественном языке, три встроенных типа (default, worker, explorer), кастомные TOML-агенты, мониторинг через /agent, CSV-батчи. Pi: расширение subagent, агенты в .pi/agents/, оркестрация шаблонами промптов. Все три подхода выручают при куче файлов, нескольких независимых задачах или потребности в свежем взгляде. Параллельные правки файлов изолируй git worktree.

Дальше — hands-on мультиагентный воркфлоу.

Юнит 4 · Квиз

Квиз 1: концепции сабагентов

Проверяем понимание паттернов, архитектур и уместности сабагентов.

Что такое сабагент?

Что такое паттерн fan-out / fan-in?

Что такое паттерн «пайплайн»?

Когда тянуться за сабагентами?

Что такое паттерн «супервизор»?

Итоги

4–5 верных — ядро паттернов у тебя на месте. Если нет — вернись к главе про паттерны до практики.

Запомнить: сабагенты — изолированные дочерние агенты, а не другие платформы и не фоновые скиллы; fan-out/fan-in параллелен, пайплайн последователен, супервизор координирует специалистов; лучший сигнал — независимая или чётко постадийная работа, оправдывающая координационные издержки.

Юнит 4 · Практика

Практика: мультиагентный воркфлоу

Проект: пайплайн «ресёрч → реализация → верификация». Сабагент-исследователь мапит существующий код, реализатор пишет изменение, а два read-only ревьюера (безопасность и производительность) параллельно дают добро перед мержем.

Настройка проекта

mkdir code-quality-pipeline
cd code-quality-pipeline

# Создаём файлы
mkdir -p .claude/agents
touch main.py
touch .claude/CLAUDE.md
mkdir code-quality-pipeline
cd code-quality-pipeline

# Создаём файлы
mkdir -p .codex/agents
touch main.py
touch AGENTS.md
mkdir code-quality-pipeline
cd code-quality-pipeline

# Создаём файлы
mkdir -p .pi/agents .pi/extensions/subagent .pi/prompts
touch main.py
touch AGENTS.md

Наполни .pi/extensions/subagent/ официальным примером сабагентов Pi (или своим аналогом) до запуска.

Шаг 1. Определяем кастомных агентов

Воркфлоу на всех платформах один: узкий исследователь, писатель кода и два read-only ревьюера. Отличается формат файлов.

.claude/agents/researcher.md:

---
name: researcher
description: Исследует кодовую базу и документирует архитектуру
tools: Read, Grep, Glob
model: sonnet
---
Ты — исследователь архитектуры. Твоя работа:
1. Исследовать структуру кодовой базы
2. Найти ключевые модули, зависимости и паттерны
3. Ясно задокументировать архитектуру
4. Отметить зоны для улучшения

Будь дотошным. Прочитай 10+ файлов для полной картины.

.claude/agents/implementer.md:

---
name: implementer
description: Пишет код по спецификациям
tools: Read, Write, Glob, Bash
model: sonnet
---
Ты — опытный разработчик. Твоя работа:
1. Прочитать спецификацию и доки по архитектуре
2. Написать чистый, покрытый тестами код
3. Следовать существующим паттернам и конвенциям
4. Задокументировать изменения

Фокус — корректность и поддерживаемость.

.claude/agents/security-reviewer.md:

---
name: security-reviewer
description: Ревьюит код на уязвимости
tools: Read, Grep
model: sonnet
---
Ты — эксперт по безопасности. Твоя работа:
1. Отревьюить код на уязвимости
2. Проверить утечки данных, инъекции и т.п.
3. Порекомендовать фиксы
4. Оценить общий уровень безопасности

Будь дотошным: этот код может быть продакшн-критичным.

.claude/agents/performance-reviewer.md:

---
name: performance-reviewer
description: Ревьюит код на проблемы производительности
tools: Read, Grep
model: sonnet
---
Ты — специалист по производительности. Твоя работа:
1. Найти узкие места
2. Предложить оптимизации
3. Оценить эффект
4. Порекомендовать бест-практики

Давай конкретику: цифры и предметные предложения.

.codex/agents/researcher.toml:

name = "researcher"
description = "Read-only исследователь кодовой базы: мапит архитектуру до реализации."
sandbox_mode = "read-only"
developer_instructions = """
Оставайся в режиме исследования.
Читай широко, отслеживай реальный путь выполнения и суммаризируй
паттерны, прежде чем предлагать изменения.
"""

.codex/agents/implementer.toml:

name = "implementer"
description = "Агент реализации: пишет минимальное обоснованное изменение кода."
developer_instructions = """
Сначала прочитай спецификацию и заметки ресёрча.
Владей изменением кода, держи скоуп узким и валидируй то,
что модифицируешь.
"""

.codex/agents/security-reviewer.toml:

name = "security_reviewer"
description = "Read-only ревьюер: уязвимости, секреты, границы привилегий."
sandbox_mode = "read-only"
developer_instructions = """
Начинай с конкретных рисков безопасности.
Приоритет: утечки данных, инъекции, ошибки аутентификации
и небезопасные дефолты.
"""

.codex/agents/performance-reviewer.toml:

name = "performance_reviewer"
description = "Read-only ревьюер: латентность, число запросов, кеширование."
sandbox_mode = "read-only"
developer_instructions = """
Ищи дорогие циклы, повторяющиеся запросы и отсутствующие кеши.
Предпочитай конкретные узкие места общим советам про производительность.
"""

.pi/agents/researcher.md:

---
name: researcher
description: Исследует кодовую базу и документирует архитектуру
tools: read, grep, find, ls
---
Ты — исследователь архитектуры: исследуй структуру, находи ключевые
модули, зависимости и паттерны, документируй архитектуру, отмечай
зоны улучшения. Читай 10+ файлов для полной картины.

.pi/agents/implementer.md:

---
name: implementer
description: Пишет код по спецификациям
tools: read, grep, find, ls, bash, edit, write
---
Ты — опытный разработчик: читай спецификацию и доки, пиши чистый
покрытый тестами код, следуй существующим паттернам, документируй
изменения. Фокус — корректность и поддерживаемость.

.pi/agents/security-reviewer.md:

---
name: security-reviewer
description: Ревьюит код на уязвимости
tools: read, grep, find, ls
---
Ты — эксперт по безопасности: ищи уязвимости, утечки данных,
инъекции и небезопасную работу с секретами, рекомендуй фиксы,
оценивай общий уровень безопасности.

.pi/agents/performance-reviewer.md:

---
name: performance-reviewer
description: Ревьюит код на проблемы производительности
tools: read, grep, find, ls
---
Ты — специалист по производительности: находи узкие места,
предлагай оптимизации, оценивай эффект, давай конкретику
с цифрами.

Шаг 2. Политики проекта

Предпочитаемый воркфлоу фиксируется в инструкции уровня репозитория — чтобы вся команда каждый раз шла одной и той же последовательностью.

.claude/CLAUDE.md описывает пайплайн из трёх стадий и триггеры:

# Code Quality Pipeline

## Архитектура
Проект использует мультиагентный воркфлоу:
1. **Ресёрч** — понять существующий код
2. **Реализация** — написать новые фичи
3. **Верификация** — ревью безопасности и производительности

## Триггеры воркфлоу

### Перед реализацией
Когда пользователь просит новую фичу:
1. Сначала сабагент researcher
2. Он исследует кодовую базу (15+ файлов)
3. Документирует архитектуру и релевантные паттерны
4. Передаёт находки в стадию реализации

### Во время реализации
1. Запустить сабагента implementer
2. Дать ему: спецификацию фичи (от пользователя),
   находки об архитектуре (от researcher),
   существующие паттерны кода
3. Он пишет код

### Перед мержем
1. Параллельно запустить security-reviewer и performance-reviewer
2. Оба — read-only (без Write)
3. Собрать находки
4. Доложить пользователю до коммита

## Пример: добавить платёжную систему
Пользователь: «Добавь интеграцию со Stripe»
→ Researcher исследует платёжный код
→ Implementer пишет интеграцию по находкам
→ Security-reviewer проверяет утечки токенов
→ Performance-reviewer проверяет запросы к базе
→ Пользователь смотрит находки и решает мержить

AGENTS.md:

# Code Quality Pipeline

Для работы над фичами — флоу research -> implement -> verify.

- Сначала запускай `researcher`: замапить затронутые пути
  и задокументировать текущие паттерны.
- Потом `implementer` — он владеет изменением.
- Перед мержем параллельно `security_reviewer` и
  `performance_reviewer`, дождаться обоих.
- Ревью-агенты — read-only; находки докладывать до коммита.

AGENTS.md — то же самое: research → implement → verify, ревьюеры read-only и параллельно, находки до коммита. Если используешь официальный пример расширения — добавь проектные шаблоны промптов в .pi/prompts/ для типовых флоу вроде «research → implement → review».

Шаг 3. Гоняем пайплайн

Сценарий: добавить фичу. Промпт агенту:

Хочу добавить OAuth2-аутентификацию.

1. Используй сабагента researcher: исследуй текущую auth-систему.
   Прочитай auth-модуль тщательно (15+ файлов).
   Задокументируй текущую архитектуру и паттерны.

2. Используй сабагента implementer: напиши OAuth2-интеграцию.
   Основывайся на находках об архитектуре.
   Следуй существующим паттернам.

3. Когда готово — пусть security-reviewer и performance-reviewer
   отревьюят код параллельно.
   Хочу видеть уязвимости и узкие места до мержа.
Хочу добавить OAuth2-аутентификацию.

Сначала запусти `researcher` — пусть замапит текущую auth-систему
по 15+ файлам.
Потом запусти `implementer` — пусть добавит OAuth2-интеграцию
по этим находкам.
В конце параллельно запусти `security_reviewer` и
`performance_reviewer`, дождись обоих и суммаризируй риски
перед мержем.
Хочу добавить OAuth2-аутентификацию.

Используй `researcher`, чтобы замапить текущую auth-систему
по 15+ файлам.
Потом `implementer` — пусть добавит OAuth2-интеграцию по находкам.
В конце параллельно прогони `security-reviewer` и
`performance-reviewer`, дождись обоих и суммаризируй риски
перед мержем.

Типичный результат:

Шаг 1: запускаю сабагента researcher...
  ✓ Исследую каталог auth/
  ✓ Читаю 18 релевантных файлов
  ✓ Документирую паттерны и зависимости
  → Находки: система на JWT с кешем в Redis.
             Ротация токенов раз в час.
             Валидация на каждом API-вызове.

Шаг 2: запускаю сабагента implementer...
  ✓ Пишу интеграцию с OAuth2-провайдером
  ✓ Следую существующему JWT-паттерну
  ✓ Добавил тесты
  → Реализация: 450 строк нового кода + тесты

Шаг 3: параллельно запускаю security-reviewer и performance-reviewer...
  ✓ Security-ревью готово
    Находки: HTTPS форсирован? ✓ Токены хешируются? ✓
    Секретов в логах нет? ✓
    Рекомендация: добавить rate limiting на OAuth-эндпоинт

  ✓ Performance-ревью готово
    Находки: 3 запроса к базе на аутентификацию (ожидаемо)
    Рекомендация: кешировать метаданные OAuth-провайдера

Готово к мержу? Смотри находки выше.

Шаг 4. Пример fan-out

На ресёрче родитель может развернуться в параллельные исследовательские потоки:

Ресёрч структуры кодовой базы

[Ресёрч-сабагент 1: backend/]      [2: frontend/]      [3: infrastructure/]
├─ authentication/                 ├─ components/      ├─ docker/
├─ database/                       ├─ state/           ├─ kubernetes/
└─ api/                            └─ pages/           └─ monitoring/

Все 3 работают параллельно → родитель сводит находки

Промпт:

Используй 3 сабагентов для ресёрча кодовой базы:
- Ресёрчер 1: архитектура бэкенда
- Ресёрчер 2: архитектура фронтенда
- Ресёрчер 3: устройство инфраструктуры

Каждый исследует 15+ файлов и документирует паттерны.
Потом сведи находки в обзор архитектуры.

Шаг 5. Пайплайн-ревью

Классический пайплайн: дизайн → код → тест → деплой.

Стадия 1: Дизайн архитектуры
└─ Сабагент-дизайнер: схема, интерфейсы

Стадия 2: Реализация
└─ Сабагент-разработчик: код по дизайну

Стадия 3: Security-ревью
└─ Сабагент-безопасник: поиск уязвимостей

Стадия 4: Тестирование
└─ QA-сабагент: пишет и гоняет тесты

Стадия 5: Планирование деплоя
└─ DevOps-сабагент: стратегия деплоя

Родитель: сводит находки и показывает пользователю

Команда:

Используй пайплайн-воркфлоу для нового API-эндпоинта:

Стадия 1: спроектируй схему эндпоинта и форматы запроса/ответа
Стадия 2: реализуй эндпоинт по дизайну
Стадия 3: security-ревью реализации
Стадия 4: напиши полноценные тесты
Стадия 5: спланируй деплой

Каждая стадия — сабагент. Последовательно
(выход стадии N → вход стадии N+1).

Продвинутое: автоматизация отдельно от делегирования

Репо-инструкции определяют, когда использовать ревью-агентов. Если нужна ещё и автоматизация — используй настоящую автоматизационную поверхность платформы, а не изобретай DSL для сабагентов.

.claude/CLAUDE.md — для делегирования. Для автоматических проверок — хуки в .claude/settings.json, например запуск тестов или линтера:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run-checks.sh"
          }
        ]
      }
    ]
  }
}

AGENTS.md — для делегирования. Рантайм-лимиты вроде числа тредов — в .codex/config.toml, а именованных агентов запрашивай явно.

AGENTS.md и шаблоны промптов — для делегирования. Автоматизация — в расширениях, например .pi/extensions/pipeline-guard.ts:

import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  pi.on("tool_call", async (event) => {
    if (event.toolName !== "bash") return;

    const command = event.input.command as string;
    if (/rm -rf|git reset --hard/.test(command)) {
      return { block: true, reason: "Опасная команда заблокирована политикой пайплайна" };
    }
  });
}

Пример итоговых результатов

{
  "researcher": {
    "architecture": {
      "modules": ["auth", "api", "database"],
      "patterns": ["MVC", "Repository pattern", "Middleware"],
      "dependencies": ["Express.js", "MongoDB", "Redis"]
    }
  },
  "implementer": {
    "files_created": ["oauth2.js", "oauth2.test.js"],
    "lines_of_code": 450,
    "test_coverage": 94
  },
  "security_reviewer": {
    "vulnerabilities": 0,
    "warnings": 1,
    "recommendations": ["Добавить rate limiting"]
  },
  "performance_reviewer": {
    "database_queries_per_request": 3,
    "bottlenecks": 0,
    "recommendations": ["Кешировать метаданные провайдера"]
  }
}

Что демонстрирует проект

Чистое разделение стадий (ресёрч, реализация, верификация); специализированные агенты с конкретными инструментами и промптами; параллельное выполнение ревью безопасности и производительности; пайплайн, где выход каждой стадии питает следующую; и репо-инструкции, которые держат воркфлоу единым для всей команды.

Ключевое Researcher исследует и документирует, implementer пишет код, ревьюеры безопасности и производительности параллельно дают добро. Специалисты определяются в .claude/agents/*.md, .codex/agents/*.toml или .pi/agents/*.md, воркфлоу живёт в CLAUDE.md или AGENTS.md. Хуки, расширения и настройки рантайма — для автоматизации вокруг воркфлоу, а не для оркестрации сабагентов.

Дальше — квизы юнита 4.

Юнит 4 · Квиз

Квиз 2: мультиагентные воркфлоу

Проверяем умение проектировать и строить мультиагентные воркфлоу.

Когда накладные расходы сабагентов оправданы?

Как родитель делится контекстом с сабагентом?

Как проектировать с учётом сбоев сабагентов?

Как Claude Code и Codex выставляют сабагентов?

Какие встроенные типы агентов есть у Codex?

Итоги юнита 4

4–5 верных — ты готов проектировать мультиагентные воркфлоу и не хвататься за сабагентов без нужды. Если промахнулся в нескольких — повтори платформенные паттерны вызова и обработку сбоев.

Запомнить: используй сабагентов, когда форма задачи оправдывает изоляцию или параллелизм; передавай ребёнку только нужный контекст, а большие данные — файлами; закладывай ретраи, частичные результаты и платформенные детали вызова с самого начала.

Юнит 4 пройден! Дальше — юнит 5: детерминированные хуки жизненного цикла вокруг этих же агентских воркфлоу.

Юнит 5 · Хуки

Введение: что такое хуки

Хуки — пользовательские обработчики, которые выполняются в детерминированных точках жизненного цикла агента. Когда агент собирается вызвать инструмент, завершить ход или начать новую сессию, рантайм ставит паузу, вызывает все хуки, зарегистрированные на это событие, и продолжает. Хуки позволяют наблюдать за действиями агента, блокировать небезопасные операции, подмешивать дополнительный контекст или стримить события во внешнюю систему. Ключевое — всё это без просьб к модели сделать это самой.

Промпт пользователя
    │
    ├─[хук UserPromptSubmit]─► логирование / инъекция контекста
    ▼
Рассуждение модели
    │
    ├─[хук PreToolUse]─► разрешить, запретить, переписать аргументы
    ▼
Выполнение инструмента
    │
    ├─[хук PostToolUse]─► лог, анализ, пост-обработка
    ▼
Модель продолжает
    │
    └─[хуки Stop / SessionEnd]─► сохранить, уведомить, прибраться

Скиллы, MCP, плагины и сабагенты определяют, что агент может делать. Хуки определяют, что происходит вокруг каждого его шага. Поэтому хуки — правильная поверхность для наблюдаемости, ограждений (guardrails) и автоматизационного клея.

Почему хуки важны

Модель, которую попросили «всегда запускай линтер после правки», рано или поздно забудет — или будет делать это непоследовательно. Хук на PostToolUse, запускающий линтер, срабатывает каждый раз без исключений. Хуки превращают конвенции в код.

Справка Линтер — инструмент, который проверяет код на типичные ошибки, проблемы стиля и нарушения правил проекта. Обычно он не запускает программу целиком — сканирует код и сообщает о подозрительном и непоследовательном.

Больше всего выигрывают три паттерна:

  • Наблюдаемость. Каждый вызов инструмента, промпт и стоп-событие можно писать в лог или дашборд — и видеть, что агент реально сделал, а не что он сказал, что сделал.
  • Ограждения. Хук может инспектировать Bash-команду до выполнения и запретить всё, что трогает ~/.ssh/ или содержит rm -rf /. Ограждения кодом надёжнее ограждений промптом.
  • Автоматизация. Хуки автоматически гоняют форматтеры, линтеры, тайп-чекеры и тесты после правки файла. Агенту не нужно помнить — помнит рантайм.

Ландшафт платформ

Все четыре платформы курса поддерживают хуки, но в разных формах:

  • Claude Code — JSON-конфигурация в .claude/settings.json (или внутри плагина). Имена событий в PascalCase: PreToolUse, Stop. Обработчики — шелл-команды, HTTP-эндпоинты, промпты или сабагенты.
  • Codex — JSON-конфигурация в .codex/hooks.json, за фича-флагом в config.toml. Событий меньше (тоже PascalCase), обработчики — шелл-команды с JSON-нагрузкой на stdin.
  • OpenCode — TypeScript/JavaScript-модули плагинов в .opencode/plugins/. Хуки — ключи объекта, который экспортирует плагин: "tool.execute.before" или обобщённый колбэк event. JSON-конфига событий нет — всё кодом.
  • Pi — TS/JS-расширения в .pi/extensions/ или ~/.pi/agent/extensions/. События в lower_snake_case: before_agent_start, tool_call, tool_result; обработчики регистрируются кодом через pi.on(...).

Ментальная модель везде одна: события срабатывают, обработчики выполняются и могут повлиять на следующий шаг агента. Меняются только синтаксис и имена событий.

Что будем строить

Юнит проходит по жизненному циклу хуков, показывает точную форму конфига для каждой платформы и собирает рабочий дашборд активности агента: Gradio-приложение, принимающее хук-события по HTTP и визуализирующее вызовы инструментов, промпты и сессии в реальном времени. К концу у тебя будет один дашборд, работающий со всеми четырьмя агентами.

Дальше — экскурсия по самим хук-событиям.

Юнит 5 · Хуки

События хуков и жизненный цикл агента

Прежде чем что-то подключать, полезно знать, какие события вообще срабатывают и когда. Словарь на каждой платформе чуть свой, но жизненный цикл, который они описывают, общий.

Интерактив · общий жизненный цикл сессии агента

SessionStart
UserPromptSubmit
PreToolUse
PostToolUse
Stop
SessionEnd
1/6

События по платформам

У Claude Code самый богатый набор событий из четырёх платформ. Всё в PascalCase, конфигурируется в .claude/settings.json (или в hooks/hooks.json плагина).

Ядро жизненного цикла: SessionStart (новая сессия — свежая, resume или продолжение после компакции), InstructionsLoaded (CLAUDE.md-файлы загружены), UserPromptSubmit (промпт отправлен, модель его ещё не видела), PreToolUse (перед вызовом инструмента), PermissionRequest (сейчас покажется запрос разрешения), PermissionDenied (вызов инструмента отклонён классификатором авторежима), PostToolUse (после вызова), PostToolUseFailure (вызов упал), Stop (ход завершён), SessionEnd (сессия закрыта).

Сабагенты и задачи: SubagentStart, SubagentStop (делегированный сабагент стартует/финиширует), TaskCreated, TaskCompleted (изменился список задач TodoWrite).

Окружение: CwdChanged, FileChanged (сменился рабочий каталог или отслеживаемый файл), WorktreeCreate, WorktreeRemove (git worktree создан/удалён), PreCompact, PostCompact (компакция контекста вот-вот случится / только что закончилась), ConfigChange, Notification (настройки изменились; Claude поднял уведомление).

Группы матчеров фильтруют по полям события: инструментальные события матчатся по именам инструментов ("matcher": "Bash" или "matcher": "Edit|Write"), а условия if уровня обработчика могут использовать синтаксис permission-правил для инспекции аргументов (например, "if": "Bash(rm *)").

Хуки Codex экспериментальны и включаются явно — в ~/.codex/config.toml:

[features]
codex_hooks = true

Codex поставляет компактный набор событий (все PascalCase), конфигурация в ~/.codex/hooks.json или <repo>/.codex/hooks.json:

  • SessionStart — сессия начинается; матчер фильтрует поле source: startup, resume или clear.
  • UserPromptSubmit — промпт отправлен.
  • PreToolUse — перед поддерживаемым вызовом инструмента: Bash, apply_patch (алиасы Edit|Write) и MCP-инструменты.
  • PermissionRequest — сейчас покажется запрос разрешения.
  • PostToolUse — после поддерживаемого вызова.
  • Stop — ход завершён.

Набор событий Codex сознательно близок к классическому жизненному циклу Claude Code — кросс-агентные хук-скрипты легче шарить. Но поверхность инструментальных событий уже, чем у Claude Code, и перехватывает не каждый путь вызова инструментов. Матчеры — регэкспы по tool_name (для инструментальных событий) или source (для SessionStart). Хуки Codex быстро меняются — детали могут отличаться между версиями.

У OpenCode нет JSON-конфига событий. Плагины — TS/JS-модули в .opencode/plugins/, и каждый экспортируемый плагин возвращает объект, ключи которого — имена событий. Рантайм вызывает подходящий ключ, когда событие срабатывает.

Типизированные хук-ключи (первоклассные): "tool.execute.before" (инструмент вот-вот выполнится), "tool.execute.after" (выполнение завершено), "shell.env" (запускается шелл-команда; можно мутировать окружение), "experimental.session.compacting" (сессия компактится).

Обобщённый колбэк event получает { event } с полем event.type. Типы включают события жизненного цикла и UI: session.created, session.updated, session.idle, session.compacted, session.deleted, session.error, session.diff, session.status; message.updated, message.removed, message.part.updated, message.part.removed; command.executed, file.edited, file.watcher.updated; permission.asked, permission.replied; lsp.client.diagnostics, lsp.updated; todo.updated, server.connected, installation.updated; tui.prompt.append, tui.command.execute, tui.toast.show.

События UserPromptSubmit у OpenCode нет. Ближайший эквивалент — читать новое сообщение пользователя через обобщённый колбэк на message.updated.

Хуки Pi живут внутри TS/JS-расширений в .pi/extensions/ или ~/.pi/agent/extensions/. Встроенный жизненный цикл — в lower_snake_case:

Ядро: session_start (сессия начинается: startup, reload, new, resume или fork), before_agent_start (получен промпт; можно инъектировать сообщения или править системный промпт), agent_start (стартует агентский цикл), turn_start/turn_end (один ход модели начинается/заканчивается), tool_call (перед выполнением инструмента; можно мутировать аргументы или блокировать), tool_result (после выполнения; можно переписать результат), agent_end (запрос завершён), session_shutdown (рантайм расширений сворачивается).

Сессии и управление: session_before_switch, session_before_fork (перехват /new, /resume, /fork, /clone), session_before_compact, session_compact, session_before_tree, session_tree (управление контекстом и навигация по дереву), model_select, user_bash (смена модели и пользовательские шелл-команды).

JSON-файла хуков у Pi нет: обработчики регистрируются кодом через pi.on("event_name", handler) и распространяются в расширении или Pi-пакете.

Что получает хук на вход

Командные хуки получают JSON на stdin; HTTP-хуки — тот же payload телом POST-запроса. Общие поля всех событий:

{
  "session_id": "...",
  "transcript_path": "/path/to/transcript.jsonl",
  "cwd": "/path/to/project",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse"
}

Инструментальные события добавляют tool_name, tool_input и (на PostToolUse) tool_response. События сабагентов — agent_id и agent_type. Корень проекта доступен и как env-переменная CLAUDE_PROJECT_DIR.

Командные хуки получают JSON на stdin. Общие поля:

{
  "session_id": "...",
  "transcript_path": "/path/to/transcript.jsonl",
  "cwd": "/path/to/project",
  "hook_event_name": "PreToolUse",
  "model": "gpt-5-codex"
}

События уровня хода добавляют turn_id. Инструментальные — tool_name, tool_use_id, tool_input; для Bash и apply_patch команда лежит в tool_input.command. PostToolUse добавляет tool_response, UserPromptSubmitprompt, Stopstop_hook_active и last_assistant_message.

Хуки OpenCode — JS/TS-функции, а не stdin-скрипты. У каждого типа события — типизированная сигнатура (input, output):

"tool.execute.before": async (input, output) => {
  // input.tool       — имя инструмента (например, "read", "bash")
  // input.sessionID  — идентификатор сессии
  // output.args      — аргументы инструмента (мутабельные)
}

"tool.execute.after" добавляет результат в output. "shell.env" даёт мутировать output.env. Обобщённый колбэк event получает { event } с типом в event.type. Плагин также получает объект контекста при создании: { project, directory, worktree, client, $ }client.app.log({...}) для структурированного логирования, $ (Bun-шелл) для команд.

Хуки Pi — TS/JS-функции в расширении. Типичные формы событий:

pi.on("before_agent_start", async (event, ctx) => {
  // event.prompt        — текст промпта пользователя
  // event.systemPrompt  — системный промпт этого хода
  // event.images        — приложенные изображения (если есть)
});

pi.on("tool_call", async (event, ctx) => {
  // event.toolName      — "bash", "read", "write" и т.д.
  // event.toolCallId    — уникальный id вызова
  // event.input         — мутабельные аргументы инструмента
});

pi.on("tool_result", async (event, ctx) => {
  // event.toolName      — имя инструмента
  // event.input         — финальные аргументы
  // event.content       — блоки контента результата
  // event.details       — структурированные детали
  // event.isError       — упал ли инструмент
});

Для вложенной асинхронщины внутри обработчика используй ctx.signal — тогда Esc сможет отменить и fetch()-вызовы самого расширения.

Как хуки влияют на агента

Хуки — не только наблюдение: они могут менять то, что случится дальше.

Коды выхода (командные хуки): exit 0 — разрешить без изменений; exit 2 с сообщением в stderr — заблокировать или продолжить в зависимости от семантики события (на PreToolUse блокирует вызов инструмента, на UserPromptSubmit стирает промпт).

JSON на stdout для тонкого контроля:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "В этом проекте нет доступа к сети"
  }
}

Также поддерживаются верхнеуровневые continue, stopReason, suppressOutput, systemMessage и легаси-форма { "decision": "block", "reason": "..." }.

HTTP-хуки возвращают тот же JSON телом 2xx-ответа. Не-2xx-ответы и таймауты трактуются как неблокирующие ошибки.

Коды выхода: 0 — успех; 2 с сообщением в stderr — блокировка или продолжение по семантике события.

JSON на stdout — форма, похожая на Claude Code:

{
  "systemMessage": "Контекст, инъектированный для модели",
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "В этом проекте нет доступа к сети"
  }
}

Для SessionStart, UserPromptSubmit и PostToolUse в hookSpecificOutput можно передать additionalContext — текст, инъектируемый в разговор.

Хуки OpenCode влияют на агента мутацией объекта output или выбросом ошибки. Переписать вызов инструмента — мутируй output.args; заблокировать — кидай ошибку:

"tool.execute.before": async (input, output) => {
  if (input.tool === "bash" && /rm -rf/.test(output.args.command ?? "")) {
    throw new Error("опасная команда заблокирована политикой")
  }
}

Ни кодов выхода, ни JSON-через-stdin — всё влияние через код.

Хуки Pi влияют на агента возвратом структурированных значений или мутацией состояния события: верни { block: true, reason: "..." } из tool_call, чтобы отменить инструмент; мутируй event.input внутри tool_call, чтобы переписать аргументы; верни { content, details, isError } из tool_result, чтобы переписать результат; верни message или systemPrompt из before_agent_start, чтобы инъектировать контекст в следующий ход.

pi.on("tool_call", async (event) => {
  if (event.toolName === "bash" && /rm -rf/.test(event.input.command as string)) {
    return { block: true, reason: "опасная команда заблокирована политикой" };
  }
});

pi.on("before_agent_start", async (event) => ({
  systemPrompt: event.systemPrompt +
    "\n\nВсегда объясняй рискованные шелл-команды перед запуском.",
}));

Как выбрать событие

Быстрая шпаргалка под типовые цели:

  • Логировать каждый вызов инструментаPreToolUse / tool.execute.before / tool_call.
  • Гонять линтер после правкиPostToolUse (матчер Edit|Write) / tool.execute.after / tool_result.
  • Блокировать опасные командыPreToolUse с exit-кодом 2, выброшенная ошибка или tool_call с { block: true }.
  • Инъектировать репо-контекст на каждом ходуUserPromptSubmit с additionalContext (Claude Code / Codex), колбэк event на message.updated (OpenCode) или before_agent_start (Pi).
  • Сохранять состояние разговораStop / SessionEnd, session.idle в OpenCode или agent_end / session_shutdown в Pi.
  • Добавлять env-переменные шелламshell.env (OpenCode) или мутация в tool_call / обёртка user bash в расширении Pi.
Ключевое Платформы выставляют один и тот же жизненный цикл под разным словарём и с разной гранулярностью. У Claude Code — богатейший набор событий и четыре типа обработчиков. У Codex — компактный набор за фича-флагом. OpenCode складывает хуки в систему плагинов как типизированные функции-ключи. Pi — события расширений вроде before_agent_start, tool_call, tool_result. Выбрал событие под цель — дальше работа везде одинаковая.

Дальше — короткий квиз, а потом подключим эти события к живому Gradio-дашборду.

Юнит 5 · Квиз

Квиз 1: основы хуков

Проверяем понимание хуков, их событий и различий между Claude Code, Codex, OpenCode и Pi.

Что такое хук?

Чем различаются поверхности хуков на платформах?

Какое утверждение про события жизненного цикла верно?

Как хуки блокируют или модифицируют действия?

Когда хуки вместо скиллов?

Итоги

4–5 верных — модель хуков ты понимаешь достаточно, чтобы строить настоящие ограждения и наблюдаемость. Если нет — повтори жизненный цикл событий и сравнение платформ.

Запомнить: хуки — детерминированные обработчики рантайма, а не промпты и не плагины; четыре платформы выставляют похожие моменты жизненного цикла через разные поверхности; хуки — правильный инструмент, когда поведение должно происходить каждый раз, а не «когда модель вспомнит».

Дальше — подключаем хуки к настоящему дашборду.

Юнит 5 · Практика

Практика: дашборд активности агента на Gradio

Проект подключает хуки из предыдущего урока к живому дашборду. Дашборд — Gradio-приложение, которое принимает хук-события по HTTP, показывает последние вызовы таблицей и рисует использование инструментов во времени. Работает с Claude Code, Codex, OpenCode и Pi: один и тот же агент делает работу — а ты видишь, что реально происходит под капотом.

Что будем строить

Один Python-процесс, который делает две вещи сразу:

  1. FastAPI-эндпоинт POST /event, куда может постить любой хук.
  2. Gradio-приложение на том же сервере, рендерящее события вживую.

Gradio опрашивает in-memory буфер событий и перерисовывается каждую секунду — агент виден в реальном времени.

Настройка проекта

mkdir agent-activity-dashboard
cd agent-activity-dashboard

pip install "gradio>=4.41" "fastapi" "uvicorn[standard]" "pandas"

requirements.txt для будущего деплоя:

gradio>=4.41
fastapi
uvicorn[standard]
pandas

Шаг 1. Приёмник и дашборд

app.py:

import datetime as dt
from collections import Counter, deque
from typing import Any

import gradio as gr
import pandas as pd
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

# ---------- Общее состояние ----------
MAX_EVENTS = 500
events: deque[dict[str, Any]] = deque(maxlen=MAX_EVENTS)


def _truncate(value: Any, n: int) -> str:
    text = "" if value is None else str(value)
    return text if len(text) <= n else text[: n - 1] + "…"


def _normalize(body: dict[str, Any], headers: dict[str, str]) -> dict[str, Any]:
    """Свести payload'ы Claude Code / Codex / OpenCode / Pi к одной форме."""
    platform = (
        body.get("platform")
        or headers.get("x-platform")
        or "unknown"
    )
    event_name = body.get("event") or body.get("hook_event_name") or "Unknown"
    tool = body.get("tool") or body.get("tool_name") or ""
    args = body.get("args") or body.get("tool_input") or body.get("prompt") or ""
    return {
        "timestamp": dt.datetime.utcnow().isoformat(timespec="seconds") + "Z",
        "platform": str(platform),
        "event": str(event_name),
        "tool": str(tool),
        "args": _truncate(args, 200),
    }


# ---------- FastAPI-приёмник ----------
api = FastAPI(title="Agent Activity Dashboard")


@api.post("/event")
async def event(req: Request):
    try:
        body = await req.json()
    except Exception:
        body = {}
    record = _normalize(body, {k.lower(): v for k, v in req.headers.items()})
    events.appendleft(record)
    # Пустое тело в ответе: хуки никогда не должны блокироваться на приёмнике.
    return JSONResponse({})


@api.get("/health")
def health():
    return {"ok": True, "events": len(events)}


# ---------- Gradio-представления ----------
COLUMNS = ["timestamp", "platform", "event", "tool", "args"]


def events_df() -> pd.DataFrame:
    if not events:
        return pd.DataFrame(columns=COLUMNS)
    return pd.DataFrame(list(events), columns=COLUMNS)


def tool_counts_df() -> pd.DataFrame:
    counter = Counter(e["tool"] for e in events if e["tool"])
    rows = [{"tool": tool, "count": n} for tool, n in counter.most_common(15)]
    return pd.DataFrame(rows, columns=["tool", "count"])


def summary_md() -> str:
    total = len(events)
    platforms = sorted({e["platform"] for e in events}) or ["(нет)"]
    tools = sorted({e["tool"] for e in events if e["tool"]})
    tools_display = ", ".join(tools) if tools else "(нет)"
    return (
        f"**Событий:** {total} (буфер до {MAX_EVENTS})  \n"
        f"**Платформы:** {', '.join(platforms)}  \n"
        f"**Инструменты:** {tools_display}"
    )


def refresh():
    return events_df(), tool_counts_df(), summary_md()


def clear_events():
    events.clear()
    return refresh()


with gr.Blocks(title="Agent Activity Dashboard") as ui:
    gr.Markdown("# Дашборд активности агента")
    gr.Markdown(
        "Наведи хуки/расширения Claude Code, Codex, OpenCode или Pi на "
        "`POST http://localhost:8000/event` — активность появится здесь."
    )

    header = gr.Markdown(value=summary_md())

    with gr.Row():
        clear_btn = gr.Button("Очистить события", variant="secondary")

    chart = gr.BarPlot(
        value=tool_counts_df(),
        x="tool",
        y="count",
        title="Использование инструментов",
        tooltip=["tool", "count"],
        height=280,
    )

    table = gr.Dataframe(
        value=events_df(),
        headers=COLUMNS,
        label="Последние события (новые сверху)",
        wrap=True,
        interactive=False,
    )

    # Опрашиваем общий буфер раз в секунду.
    gr.Timer(1.0).tick(refresh, outputs=[table, chart, header])
    clear_btn.click(clear_events, outputs=[table, chart, header])


# Монтируем Gradio на FastAPI: /event и UI живут в одном процессе.
app = gr.mount_gradio_app(api, ui, path="/")


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8000)

Запуск:

python app.py

Открой http://localhost:8000. Дашборд пуст — так и должно быть, пока хук не пришлёт событие.

Совет Маршрут FastAPI для /event определён до монтирования Gradio на /, поэтому POST-ы попадают в приёмник, а всё остальное — в Gradio-UI.

Шаг 2. Подключаем агента

.claude/settings.json — HTTP-хуки на пять событий, каждый постит на приёмник с заголовком платформы:

{
  "hooks": {
    "PreToolUse":  [{ "matcher": "*", "hooks": [{ "type": "http",
      "url": "http://localhost:8000/event",
      "headers": { "X-Platform": "claude-code" } }] }],
    "PostToolUse": [{ "matcher": "*", "hooks": [{ "type": "http",
      "url": "http://localhost:8000/event",
      "headers": { "X-Platform": "claude-code" } }] }],
    "UserPromptSubmit": [{ "hooks": [{ "type": "http",
      "url": "http://localhost:8000/event",
      "headers": { "X-Platform": "claude-code" } }] }],
    "Stop":            [{ "hooks": [{ "type": "http",
      "url": "http://localhost:8000/event",
      "headers": { "X-Platform": "claude-code" } }] }],
    "SessionStart":    [{ "hooks": [{ "type": "http",
      "url": "http://localhost:8000/event",
      "headers": { "X-Platform": "claude-code" } }] }]
  }
}

Payload Claude Code уже содержит hook_event_name, tool_name и tool_input — нормализатор подхватит их без доработок. Заголовок X-Platform помечает события как claude-code.

Открой новую сессию Claude Code в каталоге проекта и попроси что-то конкретное:

Покажи файлы в этом каталоге, потом прочитай README.md и суммаризируй.

Проверь, что в ~/.codex/config.toml включён флаг:

[features]
codex_hooks = true

Примеры предполагают, что jq и curl установлены и есть в PATH. .codex/hooks.json:

{
  "hooks": {
    "PreToolUse":  [{ "matcher": "Bash", "hooks": [{ "type": "command",
      "command": "jq -c '{platform:\"codex\", event:\"PreToolUse\", tool:.tool_name, args:.tool_input}' | curl -s --max-time 2 -X POST -H 'Content-Type: application/json' --data-binary @- http://localhost:8000/event || true" }] }],
    "PostToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command",
      "command": "jq -c '{platform:\"codex\", event:\"PostToolUse\", tool:.tool_name, args:.tool_response}' | curl -s --max-time 2 -X POST -H 'Content-Type: application/json' --data-binary @- http://localhost:8000/event || true" }] }],
    "UserPromptSubmit": [{ "hooks": [{ "type": "command",
      "command": "jq -c '{platform:\"codex\", event:\"UserPromptSubmit\", args:.prompt}' | curl -s --max-time 2 -X POST -H 'Content-Type: application/json' --data-binary @- http://localhost:8000/event || true" }] }],
    "Stop":            [{ "hooks": [{ "type": "command",
      "command": "jq -c '{platform:\"codex\", event:\"Stop\", args:.last_assistant_message}' | curl -s --max-time 2 -X POST -H 'Content-Type: application/json' --data-binary @- http://localhost:8000/event || true" }] }],
    "SessionStart":    [{ "hooks": [{ "type": "command",
      "command": "jq -c '{platform:\"codex\", event:\"SessionStart\"}' | curl -s --max-time 2 -X POST -H 'Content-Type: application/json' --data-binary @- http://localhost:8000/event || true" }] }]
  }
}

Каждый хук пересобирает payload Codex в нормализованную форму {platform, event, tool, args} и постит её. --max-time 2 у curl защищает агента от зависаний на медленном дашборде, а || true делает логирование best-effort, если дашборд офлайн.

Перезапусти Codex, чтобы подхватить хуки, и попробуй:

Прогони `ls` в этом каталоге и покажи первые 20 строк app.py.

.opencode/plugins/dashboard.ts:

import type { Plugin } from "@opencode-ai/plugin"

const URL = process.env.DASHBOARD_URL ?? "http://localhost:8000/event"

async function send(event: string, payload: Record) {
  try {
    await fetch(URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ platform: "opencode", event, ...payload }),
      signal: AbortSignal.timeout(2000),
    })
  } catch {
    // дашборд может быть офлайн; никогда не блокируем инструмент
  }
}

export const DashboardPlugin: Plugin = async () => ({
  "tool.execute.before": async (input, output) =>
    send("PreToolUse", { tool: input.tool, args: output.args }),

  "tool.execute.after": async (input, output) =>
    send("PostToolUse", {
      tool: input.tool,
      args:
        typeof (output as { output?: unknown }).output === "string"
          ? ((output as { output: string }).output.slice(0, 200))
          : "",
    }),

  event: async ({ event }) => {
    if (event.type === "session.created") await send("SessionStart", {})
    if (event.type === "session.idle") await send("Stop", {})
  },
})

Если это первый OpenCode-плагин в проекте — инициализируй локальный package.json:

cd .opencode
bun init -y
bun add -d @opencode-ai/plugin

Перезапусти OpenCode (плагин грузится на старте) и попроси:

Прочитай README.md и перечисли его разделы.

.pi/extensions/dashboard.ts:

import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";

const URL = process.env.DASHBOARD_URL ?? "http://localhost:8000/event";

async function send(event: string, payload: Record) {
  try {
    await fetch(URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ platform: "pi", event, ...payload }),
      signal: AbortSignal.timeout(2000),
    });
  } catch {
    // дашборд может быть офлайн; никогда не блокируем агента
  }
}

export default function (pi: ExtensionAPI) {
  pi.on("session_start", async () => {
    await send("SessionStart", {});
  });

  pi.on("before_agent_start", async (event) => {
    await send("UserPromptSubmit", { args: event.prompt });
  });

  pi.on("tool_call", async (event) => {
    await send("PreToolUse", { tool: event.toolName, args: event.input });
  });

  pi.on("tool_result", async (event) => {
    await send("PostToolUse", {
      tool: event.toolName,
      args: event.details ?? event.content,
    });
  });

  pi.on("agent_end", async () => {
    await send("Stop", {});
  });
}

Создай .pi/extensions/ при необходимости, запусти Pi (или /reload, если он уже открыт) и попроси:

Прочитай README.md и перечисли его разделы.

Шаг 3. Смотрим вживую

Оставь python app.py в одном терминале, агента — в другом. Пока агент работает, события стримятся в дашборд:

timestamp              platform     event            tool    args
2026-04-20T10:15:02Z   claude-code  UserPromptSubmit          Покажи файлы…
2026-04-20T10:15:03Z   claude-code  PreToolUse       Bash    {"command":"ls"}
2026-04-20T10:15:03Z   claude-code  PostToolUse      Bash    {"exit_code":0,…}
2026-04-20T10:15:04Z   claude-code  PreToolUse       Read    {"path":"README…
2026-04-20T10:15:04Z   claude-code  PostToolUse      Read    {"content":"# …
2026-04-20T10:15:06Z   claude-code  Stop                     …

Бар-чарт обновляется по мере накопления инструментов — сразу видно, когда агент застрял в цикле на одном и том же инструменте.

Шаг 4. Добавляем ограждение

Дашборд-логгер уже полезен, но настоящая сила хуков — во вмешательстве. Расширим хук Claude Code блокировкой опасных команд. Добавь второй обработчик PreToolUse, который выполняется до логгера дашборда:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command // \"\"' | grep -Eq 'rm -rf|:\\(\\)\\{.*\\|.*&.*\\}:' && { echo 'blocked: dangerous shell pattern' >&2; exit 2; } || exit 0"
          }
        ]
      },
      {
        "matcher": "*",
        "hooks": [
          { "type": "http", "url": "http://localhost:8000/event",
            "headers": { "X-Platform": "claude-code" } }
        ]
      }
    ]
  }
}

Теперь одно событие PreToolUse делает две вещи: запрещает очевидно разрушительные шелл-паттерны (exit-код 2 с сообщением в stderr) и логирует всё остальное в дашборд. Создай одноразовую тестовую папку:

mkdir -p /tmp/hook-guardrail-demo-delete-me

И попроси агента выполнить:

rm -rf /tmp/hook-guardrail-demo-delete-me

Увидишь отказ ещё до выполнения команды — и событие ограждения в дашборде.

Совет Эквивалентный гард в OpenCode — throw new Error("blocked: dangerous shell pattern") внутри "tool.execute.before". В Codex хук может либо выйти с кодом 2 и причиной в stderr, либо напечатать на stdout jq -c '{hookSpecificOutput:{hookEventName:"PreToolUse", permissionDecision:"deny", permissionDecisionReason:"…"}}'.

Шаг 5. Деплой (опционально)

Для командного дашборда захости Gradio-приложение на Hugging Face Spaces: запушь app.py и requirements.txt, а URL-ы хуков поменяй с http://localhost:8000/event на https://YOUR-USERNAME-agent-dashboard.hf.space/event.

Несколько предосторожностей для общего дашборда:

  • Payload-ы могут содержать секреты (промпты, содержимое файлов, командные строки) — редактируй чувствительные поля в _normalize до записи в буфер.
  • Публичный Space — он, ну, публичный. Поставь аутентификацию перед /event или держи Space приватным и используй персональный токен в headers хука.
  • In-memory буфер сбрасывается при рестарте. Для долговременной истории замени deque на SQLite или persistent volume Spaces.

Полная раскладка проекта

agent-activity-dashboard/
├── app.py                              # Gradio + FastAPI сервер
├── requirements.txt
├── .claude/
│   └── settings.json                   # хуки Claude Code
├── .codex/
│   └── hooks.json                      # хуки Codex
├── .opencode/
│   └── plugins/
│       └── dashboard.ts                # плагин OpenCode
└── .pi/
    └── extensions/
        └── dashboard.ts                # расширение Pi

Что демонстрирует проект

Выбор правильного события для наблюдаемости (PreToolUse и PostToolUse); сведение четырёх разных форм payload к одной нормализованной записи; применение exit-кодов / hookSpecificOutput / выброшенных ошибок / возвращаемых значений для форсинга политики. Паттерн Gradio + FastAPI — один процесс, две поверхности — держит всё компактным и легко хостится на Spaces.

Ключевое Хук полезен, только если его вывод куда-то попадает. Gradio-дашборд — быстрый способ превратить сырые хук-события в то, из чего реально можно учиться: смотреть, как агент работает, где застревает, и доказывать, что ограждения срабатывают. Один дашборд работает со всеми четырьмя агентами, потому что форма события узкая, а нормализатор маленький.

Дальше — финальный квиз юнита.

Юнит 5 · Квиз

Квиз 2: хуки на практике

Проверяем умение встраивать хуки в реальные воркфлоу.

Как структурировать сервер дашборда?

Как обновляется Gradio-дашборд?

Как хуки-ограждения блокируют опасные действия?

Что делает шелл-хуки безопасными?

Почему один дашборд обслуживает несколько агентов?

Итоги юнита 5

4–5 верных — ты умеешь встраивать хуки в реальный поток наблюдаемости и ограждений. Если нет — вернись к hands-on, особенно к нормализации payload и паттернам блокировки.

Запомнить: одного процесса FastAPI + Gradio достаточно для лёгкого живого дашборда; обработчики хуков должны падать быстро, санировать payload и избегать shell-инъекций; общий приёмник работает со всеми четырьмя агентами после нормализации формы событий.

Юнит 5 пройден! Теперь у тебя пять сквозных поверхностей для формирования работы кодового агента: скиллы, MCP-серверы, плагины, сабагенты и хуки. Бонусный юнит идёт на уровень глубже — собираем агента с нуля, чтобы понять, во что все эти поверхности встраиваются.

Юнит 6 · Nano Harness

Введение в Nano Harness

Мейнстримные агентские фреймворки (Codex, Claude Code, OpenCode) прячут цикл, который заставляет их работать. Для продакшна это отлично, для обучения — нет.

Nano Harness — Python-агент на ~220 строк, написанный для чтения, а не для продакшна. Он показывает системный промпт, пошаговый цикл, выполнение инструментов, историю сообщений, обработку ошибок и песочницу — всё в одном файле.

Внимание nano_harness — учебный инструмент, не предназначенный для продакшна. Это просто весёлый способ понять, как агенты устроены под капотом. Не используй его для настоящей работы!

Зачем начинать с нуля

Собрать что-то с нуля — лучший способ понять, как оно работает под капотом. Чтение минимального харнеса делает дизайн-решения читаемыми: когда ретраить, как парсить вывод, как обрабатывать ошибки и где проходит граница безопасности. В этом юните единственная модель — zai-org/GLM-5.1 через Hugging Face Inference Providers, так что движущихся частей всего две: цикл и инструменты.

Что такое Nano Harness

Агентский мини-фреймворк, который:

  1. Берёт задачу — «Осмотри рабочее пространство и дай сводку».
  2. Вызывает LLM — через OpenAI-совместимый API (по умолчанию HF-роутер).
  3. Генерирует Python-код — модель выдаёт исполняемый код, приближающий задачу к решению.
  4. Выполняет безопасно — в песочнице с разрешёнными инструментами.
  5. Наблюдает результаты — stdout, stderr, исключения или перехваченный финальный ответ.
  6. Обновляет контекст — скармливает наблюдения обратно модели.
  7. Повторяет — пока задача не решена или не упёрлись в лимит шагов (в нашей реализации — 50).

Ключевые особенности

Code-first агент. Вместо JSON или текста модель выдаёт Python-код:

# Модель отвечает:
files = list_dir(".")
models = read_file("models.txt")
final_answer("Файлы:\n" + "\n".join(files) + "\n\nmodels.txt:\n" + models)

Агент этот код парсит и выполняет.

Ограниченные инструменты. Их четыре:

ИнструментЧто делаетБезопасность
list_dir(path)Список содержимого каталогаОграничение путей
read_file(path, max_chars)Чтение файлаОграничение путей, лимит размера
write_file(path, content)Создание/изменение файлаОграничение путей, выключен по умолчанию
exec_cmd(args)Запуск шелл-командыБелый список: ls, cat, pwd, echo, head, tail, wc, rg

Песочница выполнения: весь файловый доступ заперт в workspace; команды — только из белого списка; размер вывода ограничен (защита от переполнения контекста); таймауты против зависаний.

Модель через HF Inference Providers. Харнес по умолчанию ходит в роутер Hugging Face — OpenAI-совместимую поверхность /v1 поверх Inference Providers:

export NANO_MODEL="zai-org/GLM-5.1"
export HF_TOKEN="hf_..."

Агентский цикл

1. Вызвать LLM с задачей + историей сообщений
2. Распарсить вывод модели как Python-код
3. Выполнить Python (с доступными инструментами)
4. Собрать stdout, stderr, исключения
5. Добавить наблюдение в историю сообщений
6. Повторить (максимум 50 шагов)
7. Готово, когда: вызван final_answer() или кончились шаги

Что будет в юните

Разбор кода агентского цикла по кусочкам, устройство и песочница инструментов, расширение харнеса инструментами web_fetch и поиском по HF Hub, и запуск против zai-org/GLM-5.1 через Hugging Face Inference Providers.

Пререквизиты

Базовый Python, знакомство с HTTP API и песочницами, HF-токен с доступом к Inference Providers.

Юнит 6 · Nano Harness

Агентский цикл: глубокое погружение

Ядро nano_harness — цикл, который выполняется максимум пятьдесят раз: вызвать LLM, распарсить код, выполнить его, посмотреть на результат, повторить.

Интерактив · один виток агентского цикла

LLM
Парсер
exec() + инструменты
Наблюдение
История сообщений
1/6

Разбор кода

Конфигурация

TASK = "Осмотри рабочее пространство и дай сводку."
MODEL = os.getenv("NANO_MODEL", "zai-org/GLM-5.1")
BASE_URL = os.getenv("OPENAI_BASE_URL", "https://router.huggingface.co/v1")
API_KEY = os.getenv("HF_TOKEN", "")
WORKSPACE = str(Path.cwd())
MAX_STEPS = 50
TEMPERATURE = 0.2
TIMEOUT_S = 30
MAX_CHARS = 8000
ALLOW_WRITE = False
ALLOW_COMMANDS = ["ls", "cat", "pwd", "echo", "head", "tail", "wc", "rg"]

MODEL — ID модели HF, маршрутизируемый через Inference Providers; MAX_STEPS ограничивает итерации полусотней; TEMPERATURE=0.2 держит вывод детерминированным; ALLOW_COMMANDS — белый список шелл-команд; ALLOW_WRITE=False держит мутацию файлов выключенной по умолчанию; MAX_CHARS ограничивает вывод инструментов от переполнения контекста.

Системный промпт

SYSTEM_PROMPT = f"""You are a code-first agent.
Output only executable Python code, no prose.

Tools available:
- list_dir(path='.'): List directory contents
- read_file(path, max_chars=4000): Read file
- write_file(path, content): Write file (only if ALLOW_WRITE=True)
- exec_cmd(args): Run shell command

When task is complete, call:
  final_answer(result)

Constraints:
- All file paths confined to workspace: {WORKSPACE}
- Allowed commands: {ALLOW_COMMANDS}
- Max output: {MAX_CHARS} chars
- No markdown, no prose—only Python
"""

Системный промпт велит модели выдавать только Python, перечисляет доступные инструменты, показывает сигнал завершения (final_answer()) и фиксирует ограничения по путям, командам и размеру.

Определения инструментов

def list_dir(path="."):
    """Список содержимого каталога."""
    p = safe_path(path)  # путь обязан быть внутри workspace
    if not p.is_dir():
        raise NotADirectoryError(str(p))
    return sorted([x.name + ("/" if x.is_dir() else "") for x in p.iterdir()])

def read_file(path, max_chars=4000):
    """Чтение файла с лимитом размера."""
    p = safe_path(path)
    content = p.read_text(encoding="utf-8", errors="replace")
    return clip(content, min(max_chars, MAX_CHARS))  # режем вывод

def write_file(path, content):
    """Запись файла, если запись включена."""
    if not ALLOW_WRITE:
        raise PermissionError("write_file disabled")
    p = safe_path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(str(content), encoding="utf-8")
    return f"Записано {len(str(content))} байт"

def exec_cmd(args):
    """Шелл-команда (только из белого списка)."""
    if args[0] not in ALLOW_COMMANDS:
        raise PermissionError(f"Команда {args[0]} не разрешена")
    result = subprocess.run(args, capture_output=True, timeout=TIMEOUT_S, text=True)
    output_parts = []
    if result.stdout:
        output_parts.append(f"stdout:\n{result.stdout}")
    if result.stderr:
        output_parts.append(f"stderr:\n{result.stderr}")
    output = "\n\n".join(output_parts) or f"(код выхода {result.returncode}, вывода нет)"
    return clip(output, MAX_CHARS)

DONE = False
FINAL_RESULT = None

def final_answer(value):
    """Агент вызывает это, когда задача решена."""
    global DONE, FINAL_RESULT
    DONE = True
    FINAL_RESULT = value
    return value

Каждый инструмент форсит безопасность на своей границе: ограничение путей, белые списки команд, лимиты размера вывода и явный гейт на запись.

Главный цикл

Харнес использует Responses API: OpenAI SDK принимает message-style payload в input и возвращает response.output_text, а роутер Hugging Face выставляет ту же поверхность /v1 через Inference Providers — весь юнит крутится на одном ID модели (zai-org/GLM-5.1).

def main():
    global DONE, FINAL_RESULT
    DONE = False
    FINAL_RESULT = None
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": TASK}
    ]

    for step in range(MAX_STEPS):
        print(f"\n[Шаг {step + 1}]")

        # 1. Вызываем LLM
        response = client.responses.create(
            model=MODEL,
            temperature=TEMPERATURE,
            input=messages
        )

        content = response.output_text
        print(f"Вывод модели:\n{content[:500]}...")

        # 2. Дописываем ответ модели в историю
        messages.append({"role": "assistant", "content": content})

        # 3. Парсим и выполняем Python-код
        code = extract_python(content)  # достаём код-блок из ответа
        try:
            stdout_buffer = io.StringIO()
            stderr_buffer = io.StringIO()
            exec_globals = {
                "__builtins__": {},
                "list_dir": list_dir,
                "read_file": read_file,
                "write_file": write_file,
                "exec_cmd": exec_cmd,
                "final_answer": final_answer
            }
            with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
                exec(code, exec_globals)

            stdout_text = stdout_buffer.getvalue().strip()
            stderr_text = stderr_buffer.getvalue().strip()

            if DONE:
                result = f"Финальный ответ: {clip(FINAL_RESULT)}"
            else:
                observations = []
                if stdout_text:
                    observations.append(f"stdout:\n{clip(stdout_text)}")
                if stderr_text:
                    observations.append(f"stderr:\n{clip(stderr_text)}")
                result = "\n\n".join(observations) or "Выполнено успешно (без вывода)"
        except FileNotFoundError:
            result = "Error: FileNotFoundError: файл не найден"
        except PermissionError as e:
            result = f"Error: PermissionError: {e}"
        except subprocess.TimeoutExpired:
            result = "Error: TimeoutError: команда выполнялась слишком долго"
        except Exception as e:
            result = f"Error: {type(e).__name__}: {e}"

        # 4. Проверяем, вызвал ли агент final_answer()
        if DONE:
            print(f"✓ Задача решена: {FINAL_RESULT}")
            break

        # 5. Дописываем наблюдение в историю
        messages.append({"role": "user", "content": result})

    if not DONE:
        print(f"✗ Достигнут лимит шагов ({MAX_STEPS}) без final_answer()")

В этом цикле важны четыре вещи. История сообщений накапливает системный промпт, задачу пользователя и чередующиеся ходы «ассистент/наблюдение». Вызов LLM использует конфигурируемую модель и низкую температуру (0.2) для детерминизма. Выполнение кода идёт через exec() с урезанным словарём globals — builtins вырезаны, доступны только функции-инструменты. И цикл возвращает модели один из четырёх типов наблюдений: stdout, stderr, явный финальный ответ или структурированную строку ошибки.

Восстановление после ошибок

Главный цикл уже явно обрабатывает типовые сбои — и агент читает сбой как наблюдение и адаптируется: после отсутствующего файла — залистить каталог; при выключенной записи — перестать пытаться write_file; заблокированную команду — заменить на разрешённую; долгую работу — разбить на шаги поменьше.

Лимит шагов

Лимит шагов гарантирует завершение. Простые задачи вроде «покажи файлы» укладываются в один-три шага; исследование кодовой базы — обычно пять-десять; отладка чего-то нетривиального может занять пятнадцать-тридцать.

История сообщений — это память

messages = [
    {"role": "system", "content": "You are a code-first agent..."},
    {"role": "user", "content": "Осмотри workspace и суммаризируй"},
    {"role": "assistant", "content": "list_dir('.')\nread_file('README.md')"},
    {"role": "user", "content": "Найдено: ['README.md', 'src/', 'tests/']\n\nREADME.md содержит:\n..."},
    {"role": "assistant", "content": "read_file('src/main.py')"},
    {"role": "user", "content": "src/main.py:\n..."},
]

Каждая запись — ход: системный промпт, задача, затем чередование кода агента и наблюдений. LLM видит всю историю на каждом вызове — может ссылаться на прежние находки, не повторять провалившиеся подходы и строить на изученном.

Важная оговорка Это упрощение. Nano harness трактует память как плоскую историю разговора — каждый прошлый ход остаётся в контексте, пока окно не заполнится. Продакшн-системы используют куда более богатые архитектуры памяти: краткосрочные скретчпады для рабочего состояния, эпизодическую память для прошлых сессий, семантическую — для постоянных знаний, retrieval-подходы, достающие релевантные воспоминания по требованию, и стратегии компакции, суммаризирующие старый контекст. Хочешь глубже — смотри исследования систем памяти агентов (например, MemoryAgentBench, A-MEM) и сжатия контекста (например, ACON). Nano harness — учебный инструмент: он показывает минимальный жизнеспособный цикл, а не полную картину.

Управление контекстом

С MAX_TOKENS=4096 и MAX_CHARS=8000:

# Хорошо: агент читает по одному файлу за раз
read_file("test.py", max_chars=2000)  # 2000 символов ✓

# Плохо: агент пытается прочитать всю кодовую базу разом
read_file("large_codebase.py", max_chars=50000)  # обрежется до 8000

Агент учится читать стратегически, чтобы оставаться в лимитах.

Заметка Управление контекстом в продакшне — большая тема. Настоящие кодовые агенты реализуют компакцию (суммаризация раннего контекста), структурированные заметки (скретчпады ключевых находок), контекст через файловую систему (промежуточные результаты пишутся в файлы, а не держатся в окне) и умный выбор инструментов для экономии контекста. Гайд Anthropic по контекст-инжинирингу называет всё это ключевыми заботами серьёзного деплоя агентов. Nano harness демонстрирует лишь простейший подход: жёсткие лимиты символов и надежду, что агент читает стратегически.

Дизайн-решения

Python точен там, где JSON и свободный текст двусмысленны, — поэтому агент выдаёт код. safe_path() резолвит и валидирует каждый путь против корня workspace, отсекая directory traversal. Запускаются только команды из явного белого списка. Жёсткий лимит шагов и лимит вывода на вызов ограничивают и время работы, и рост контекста. А поскольку исключения превращаются обратно в наблюдения — агент адаптируется, а не падает.

Ключевое Вызвать LLM, распарсить код, выполнить с инструментами, понаблюдать, повторить. Системный промпт задаёт инструменты, ограничения и сигнал завершения. Песочница живёт на границе инструментов: ограничение путей, белые списки команд, лимиты размера. Ошибки становятся наблюдениями, а вся история сообщений — памятью.

Дальше — инструменты и песочница подробнее.

Юнит 6 · Nano Harness

Инструменты и песочница в деталях

Инструменты — руки агента. Nano Harness форсит свою модель безопасности на границе инструментов: ограничение путей, белые списки команд, лимиты вывода и «запись выключена по умолчанию».

Четыре инструмента

1. list_dir(path) — список файлов

def list_dir(path="."):
    """Список файлов и каталогов."""
    p = safe_path(path)  # проверяем, что путь внутри workspace
    if not p.is_dir():
        raise NotADirectoryError(str(p))
    return sorted(x.name + ("/" if x.is_dir() else "") for x in p.iterdir())

safe_path() блокирует directory traversal (например, ../../etc/passwd), возвращаются только имена файлов — не абсолютные пути, — а каталоги получают суффикс /.

list_dir(".")             # ✓ ок
list_dir("src")           # ✓ ок
list_dir("../etc")        # ✗ БЛОКИРОВАНО safe_path()

2. read_file(path, max_chars) — чтение файла

def read_file(path, max_chars=4000):
    """Чтение файла с лимитом размера."""
    p = safe_path(path)
    content = p.read_text(encoding="utf-8", errors="replace")
    # Форсим лимит уровня фреймворка
    return clip(content, min(max_chars, MAX_CHARS))  # MAX_CHARS=8000

Пути идут через safe_path(), запрошенное число символов режется по MAX_CHARS, а errors="replace" не даёт бинарным файлам уронить чтение. Даже если агент запросит max_chars=999999 — победит min(999999, 8000).

read_file("README.md")                # ✓ до 4000 символов
read_file("data.txt", max_chars=500)  # ✓ до 500 символов
read_file("huge.db")                  # ✓ обрежется до 8000 (не безлимит)
read_file("/etc/passwd")              # ✗ БЛОКИРОВАНО safe_path()

3. write_file(path, content) — запись файла

ALLOW_WRITE = False  # по умолчанию выключено!

def write_file(path, content):
    """Запись файла (за флагом ALLOW_WRITE)."""
    if not ALLOW_WRITE:
        raise PermissionError("write_file disabled")

    p = safe_path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(str(content), encoding="utf-8")
    return f"Записано {len(str(content))} байт в {p}"

Запись выключена по умолчанию — чтобы трогать диск, агенту нужен ALLOW_WRITE=True. Пути ограничены, родительские каталоги создаются по требованию.

4. exec_cmd(args) — шелл-команда

ALLOW_COMMANDS = ["ls", "cat", "pwd", "echo", "head", "tail", "wc", "rg"]

def exec_cmd(args):
    """Выполнение команды (только белый список)."""
    if args[0] not in ALLOW_COMMANDS:
        raise PermissionError(f"Команда '{args[0]}' не разрешена")

    try:
        result = subprocess.run(
            args,
            capture_output=True,
            timeout=TIMEOUT_S,  # например, 30 секунд
            text=True
        )
        output_parts = []
        if result.stdout:
            output_parts.append(f"stdout:\n{result.stdout}")
        if result.stderr:
            output_parts.append(f"stderr:\n{result.stderr}")
        output = "\n\n".join(output_parts) or f"(код выхода {result.returncode}, вывода нет)"
        return clip(output, MAX_CHARS)
    except subprocess.TimeoutExpired:
        return "Error: команда упёрлась в таймаут"

Выполняются только команды из белого списка — ls, cat, pwd, echo, head, tail, wc, rg. Всё, что мутирует состояние или лезет в сеть (rm, mv, curl, wget), отбрасывается. Таймаут сабпроцесса останавливает зависания, stdout и stderr собираются оба, суммарный вывод режется по MAX_CHARS.

exec_cmd(["ls", "-la"])      # ✓ ок (ls в списке)
exec_cmd(["pwd"])            # ✓ ок
exec_cmd(["rg", "ERROR"])    # ✓ ок (rg — это ripgrep, безопасен)
exec_cmd(["rm", "-rf", "/"]) # ✗ БЛОКИРОВАНО (rm не в списке)
exec_cmd(["curl", "evil.com"])  # ✗ БЛОКИРОВАНО (curl не в списке)

Ограничение путей: safe_path()

Всё, что принимает путь, проходит через одну функцию:

WORKSPACE = Path.cwd()  # например, /home/user/project

def safe_path(user_input):
    """Убедиться, что путь внутри workspace."""
    # Резолвим в абсолютный путь
    requested = (WORKSPACE / user_input).resolve()

    # Проверяем, что он внутри workspace
    if not requested.is_relative_to(WORKSPACE):
        raise ValueError(f"Путь {user_input} выходит за пределы workspace")

    return requested

Что это предотвращает:

# Попытка directory traversal
safe_path("../../../etc/passwd")
# → резолвится в /home/user/project/../../etc/passwd
# → резолвится в /etc/passwd
# → НЕ относительно WORKSPACE
# → ValueError ✓

# Попытка абсолютного пути
safe_path("/etc/passwd")
# → не начинается с WORKSPACE
# → ValueError ✓

# Легитимное использование
safe_path("data/models.txt")
# → резолвится в /home/user/project/data/models.txt
# → внутри WORKSPACE
# → возвращает путь ✓

Песочница выполнения

Урезанный глобальный скоуп

# Доступны только эти функции
exec_globals = {
    "__builtins__": {},  # builtins нужно вырезать явно
    "list_dir": list_dir,
    "read_file": read_file,
    "write_file": write_file,
    "exec_cmd": exec_cmd,
    "final_answer": final_answer
}

# Код агента выполняется здесь
exec(agent_code, exec_globals)

# Агент НЕ может:
# - импортировать модули (нет __builtins__)
# - трогать файлы в обход инструментов
# - напрямую ходить в сеть
# - читать переменные родительского процесса

Это намеренно строго. Если нужны хелперы вроде len() или print() — добавь маленький явный белый список безопасных builtins, а не наследуй полный питоновский набор случайно.

Сдерживание ошибок

Исключения ловятся и репортятся: сообщение об ошибке дописывается в историю как наблюдение, цикл идёт дальше — и агент адаптируется. Типичный сценарий:

Шаг 1: агент пытается прочитать огромный файл
  Код: content = read_file("huge.dat", max_chars=999999)
  Результат: (обрезано до 8000 символов — агент это видит)

Шаг 2: агент пробует запрещённую команду
  Код: exec_cmd(["rm", "data.txt"])
  Результат: PermissionError: Команда 'rm' не разрешена
  Агент видит ошибку и пробует другой подход

Шаг 3: агент пытается сбежать из workspace
  Код: read_file("../../../etc/passwd")
  Результат: ValueError: путь выходит за пределы workspace
  Агент учится и корректируется

Коммерческие агенты vs Nano Harness

Коммерческие агенты решают те же заботы за кулисами: Claude Code и Codex автоматически форсят ограничение путей, запросы разрешений, таймауты и лимиты вывода — ценой того, что правила менее видимы. Nano Harness держит всё явным: политику можно прочитать и поменять — ценой того, что реализуешь её сам.

Дизайн собственных инструментов

Расширяя nano_harness новыми инструментами, следуй паттернам:

def good_tool(user_input, max_result_size=1000):
    """
    1. Провалидировать и ограничить вход
    2. Выполнить операцию
    3. Ограничить размер вывода
    4. Вернуть безопасный результат
    """
    # 1. Валидация входа
    if not isinstance(user_input, str):
        raise TypeError("user_input должен быть строкой")

    path = safe_path(user_input)  # ограничиваем пути

    # 2. Операция
    result = path.read_text()

    # 3. Лимит вывода
    return clip(result, min(max_result_size, MAX_CHARS))

И антипаттерны:

# ✗ ПЛОХО: нет валидации входа
def bad_tool_1(path):
    return open(path).read()  # читает что угодно!

# ✗ ПЛОХО: нет лимита вывода
def bad_tool_2(query):
    return database.query(query)  # могут быть терабайты

# ✗ ПЛОХО: нет обработки ошибок
def bad_tool_3(url):
    return requests.get(url).text  # может зависнуть по таймауту

# ✗ ПЛОХО: полное доверие агенту
def bad_tool_4(command):
    os.system(command)  # агент может выполнить rm -rf /
Ключевое Ограничивай то, до чего инструменты могут дотянуться. safe_path() держит файловый доступ внутри workspace, белые списки гейтят сабпроцессы, вывод режется от взрывов контекста, запись выключена, пока её не включишь. Ошибки становятся наблюдениями — агент адаптируется, а не падает.

Дальше — расширяем nano_harness новыми инструментами и моделями.

Юнит 6 · Практика

Практика: расширяем Nano Harness

Добавим два инструмента — web_fetch и hf_search — и прогоним харнес против моделей.

Расширение 1: web_fetch

Добавь в свой nano_harness.py:

import urllib.request
import urllib.error

def web_fetch(url, max_bytes=10000):
    """Скачать содержимое веб-страницы с лимитом размера."""
    try:
        with urllib.request.urlopen(url, timeout=TIMEOUT_S) as response:
            content = response.read(max_bytes + 1)
            if len(content) > max_bytes:
                content = content[:max_bytes] + b"\n...[truncated]"
            return content.decode("utf-8", errors="replace")
    except urllib.error.URLError as e:
        return f"Error: не удалось скачать {url}: {e}"
    except Exception as e:
        return f"Error: {type(e).__name__}: {e}"

Байтовый лимит защищает от гигантских ответов, timeout — от зависаний, а ошибки возвращаются строками, чтобы агент мог отреагировать. Не забудь дописать инструмент в системный промпт: web_fetch(url, max_bytes=10000) → скачать страницу. Использование агентом:

# Агент пишет:
content = web_fetch("https://huggingface.co/")

# Или с лимитом:
content = web_fetch("https://huggingface.co/", max_bytes=5000)

Расширение 2: hf_search

def hf_search(query, resource_type="models", limit=5):
    """Поиск по Hugging Face Hub (нужен HF_TOKEN)."""
    if not API_KEY:
        return "Error: HF_TOKEN не задан — доступа к API Hugging Face нет."

    try:
        url = f"https://huggingface.co/api/{resource_type}"
        params = f"?search={query}&limit={limit}"

        req = urllib.request.Request(
            url + params,
            headers={"Authorization": f"Bearer {API_KEY}"}
        )

        with urllib.request.urlopen(req, timeout=TIMEOUT_S) as response:
            data = json.loads(response.read())

            # Форматируем результаты
            results = []
            for item in data[:limit]:
                results.append({
                    "id": item.get("id"),
                    "downloads": item.get("downloads", 0),
                    "description": item.get("description", "")[:200]
                })

            return results

    except Exception as e:
        return f"Error: {type(e).__name__}: {e}"

API-ключ живёт в окружении, результаты режутся по limit, ошибки ловятся и возвращаются. В системный промпт: hf_search(query, resource_type='models', limit=10) → поиск по HF. Использование:

# Агент пишет:
results = hf_search("bert", resource_type="models", limit=5)
final_answer(results)

Полный расширенный пример

Собери nano_harness_extended.py со всеми инструментами. Эта версия использует Responses API, так что один и тот же цикл работает и против роутера Hugging Face, и против прямого OpenAI-эндпоинта без изменения управляющей логики.

Скелет файла:

#!/usr/bin/env python3
import io
import json
import os
import re
import subprocess
import urllib.error
import urllib.request
from contextlib import redirect_stderr, redirect_stdout
from pathlib import Path
from openai import OpenAI

# Конфигурация
TASK = "Найди bert-модели на Hugging Face и суммаризируй топ-3."
MODEL = os.getenv("NANO_MODEL", "zai-org/GLM-5.1")
BASE_URL = os.getenv("OPENAI_BASE_URL", "https://router.huggingface.co/v1")
API_KEY = os.getenv("HF_TOKEN") or os.getenv("OPENAI_API_KEY", "")
WORKSPACE = str(Path.cwd())
MAX_STEPS = 50
TIMEOUT_S = 30
MAX_CHARS = 8000
ALLOW_WRITE = False
ALLOW_COMMANDS = ["ls", "cat", "pwd", "echo", "head", "tail", "wc", "rg"]
TEMPERATURE = 0.2

SYSTEM_PROMPT = f"""You are a code-first agent.
Reply with executable Python only.

Tools:
  - list_dir(path='.') → list files
  - read_file(path, max_chars=4000) → read file
  - write_file(path, content) → write file (only if ALLOW_WRITE=True)
  - exec_cmd(args) → run allowed command
  - web_fetch(url, max_bytes=10000) → fetch webpage
  - hf_search(query, limit=5) → search HF Hub

Allowed commands: {ALLOW_COMMANDS}
Writes enabled: {ALLOW_WRITE}

When done, call final_answer(result).
Output only Python code, no prose."""

def clip(x, n=MAX_CHARS):
    s = str(x)
    return s[:n] + "\n...[truncated]" if len(s) > n else s

Внутри main() — те же инструменты, что разбирали выше (safe_path, list_dir, read_file, write_file, exec_cmd), плюс новые web_fetch и hf_search, и final_answer через nonlocal done, final_result. Затем инициализация клиента и цикл:

    client = OpenAI(
        api_key=API_KEY,
        base_url=BASE_URL
    )

    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": TASK}
    ]

    for step in range(MAX_STEPS):
        print(f"\n[Шаг {step + 1}]")

        response = client.responses.create(
            model=MODEL,
            temperature=TEMPERATURE,
            input=messages
        )

        content = response.output_text
        messages.append({"role": "assistant", "content": content})

        try:
            code_match = re.search(r"```python\n(.*?)\n```", content, re.DOTALL)
            if not code_match:
                raise ValueError("Python-блок в ответе не найден")

            stdout_buffer = io.StringIO()
            stderr_buffer = io.StringIO()

            exec_globals = {
                "__builtins__": {},
                "list_dir": list_dir,
                "read_file": read_file,
                "write_file": write_file,
                "exec_cmd": exec_cmd,
                "web_fetch": web_fetch,
                "hf_search": hf_search,
                "final_answer": final_answer,
                "json": json
            }

            with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
                exec(code_match.group(1), exec_globals)

            # ... сборка наблюдения: final answer / stdout / stderr,
            # обработка FileNotFoundError, PermissionError,
            # TimeoutExpired и прочих — как в базовом цикле

        except Exception as e:
            result = f"Error: {type(e).__name__}: {e}"

        if done:
            print(f"✓ Задача решена: {final_result}")
            break

        messages.append({"role": "user", "content": result})

    if not done:
        print("✗ Достигнут лимит шагов")

Расширенная версия сохраняет модель безопасности базового харнеса: write_file по-прежнему выключен, пока не поднимешь ALLOW_WRITE; exec() работает без питоновских builtins; каждый виток возвращает stdout, stderr, финальный ответ или структурированную строку ошибки.

Запуск

export HF_TOKEN="hf_..."
export NANO_MODEL="zai-org/GLM-5.1"
python nano_harness_extended.py

Inference Providers сами маршрутизируют запрос к провайдеру. Хочешь другую HF-модель — поставь в NANO_MODEL любой text-generation-репозиторий, включённый в Inference Providers: цикл и набор инструментов не меняются.

Упражнение: расширь ещё

Добавь эти инструменты сам:

1. git_log:

# Сначала добавь "git" в ALLOW_COMMANDS, потом:
def git_log(limit=10):
    """Последние git-коммиты."""
    return exec_cmd(["git", "log", "--oneline", f"-{limit}"])

2. json_parse:

def json_parse(json_string):
    """Безопасный парсинг JSON."""
    try:
        return json.loads(json_string)
    except json.JSONDecodeError as e:
        return f"Error: {e}"

3. compute_stats:

def compute_stats(numbers):
    """Минимум, максимум, среднее."""
    nums = list(map(float, numbers))
    return {
        "min": min(nums),
        "max": max(nums),
        "mean": sum(nums) / len(nums),
        "count": len(nums)
    }

Добавь их в харнес и посмотри, как изменятся трейсы агента.

Ключевое Инструменты соединяют агента с миром. Каждому инструменту нужны лимиты размера, таймауты и информативные строки ошибок, чтобы агент мог адаптироваться. Один и тот же цикл без изменений работает против любой модели на Hugging Face Inference Providers.

Дальше — квиз юнита 6.

Юнит 6 · Квиз

Квиз: Nano Harness и внутренности агентов

Проверяем понимание агентских циклов, инструментов и песочниц.

Как устроен агентский цикл nano_harness?

Что значит «code-first» в nano_harness?

Что происходит с историей сообщений?

Что делает safe_path()?

Как exec_cmd() контролирует команды?

Финал курса

Поздравляю — ты прошёл весь курс! Теперь у тебя в руках полный стек контекст-инжиниринга: скиллы (переносимые знания), MCP (динамические инструменты и данные), плагины (упаковка и дистрибуция), сабагенты (мультиагентные воркфлоу), хуки (наблюдаемость и ограждения) — и понимание того, как всё это устроено под капотом, вплоть до минимального агентского цикла.

Не забудь: квизы юнитов 1–2 на 70%+ дают сертификат Context Fundamentals, все квизы 1–5 плюс capstone-проект — Context Engineering. Подробности — в юните 0 и на странице оригинального курса.