Read in:
Русский

Флит: агентский сайдкар

Флит — небольшой демон, который работает рядом с хабом trip2g и превращает заметки в агентов. Вы пишете заметку-роль: фронтматтер задаёт, когда запускаться и что агенту можно трогать, тело — инструкция. Положили заметку в папку агентов — агент существует. Изменили подходящую заметку в хранилище — агент запустился. Удалили заметку-роль — агента больше нет. Ни конфигов пайплайнов, ни YAML-репозитория, ни редеплоев: хранилище и есть панель управления.

Если сначала нужна теория — что такое цикл инструментов LLM и почему границы доступа должен проверять рантайм, а не промпт, — читайте Как работают LLM-агенты. Эта страница о практике: что умеет флит и как на нём собран настоящий конвейер.

В статье:

Что делает флит

Флит — сайдкар: отдельный процесс (cmd/fleet в репозитории trip2g), который подключается к хабу через обычный API. Хаб остаётся источником событий, думает флит. При старте и затем каждые 30 секунд флит сканирует папку агентов (по умолчанию roles/), разбирает каждую заметку-роль и регистрирует на хабе вебхуки на себя. Когда заметка в хранилище меняется, хаб доставляет событие флиту, флит запускает агента, а записи агента попадают обратно в хранилище обычными версиями заметок.

Под одной крышей живут два вида агентов:

  • LLM-роли (по умолчанию): модель в цикле инструментов ищет, читает и пишет заметки, пока не закончит или не упрётся в бюджет.
  • Код-роли (executor: code): программа на Python, Bash или Node в песочнице — для детерминированных шагов, которым модель не нужна.

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

Роли — это заметки

Заметка-роль — фронтматтер плюс тело. Фронтматтер — полная конфигурация агента:

---
model: gpt-4o-mini
tools: [read_note, write_note]
read_patterns: ["drafts/**"]
write_patterns: ["published/**"]
mode: change
trigger_on: [update]
trigger_include: ["drafts/**"]
for_each: changed_files
max_tokens: 4000
max_steps: 6
max_depth: 2
concurrency: skip
---
Ты редактор. Прочитай черновик по пути {{ change_file.Path }}, перепиши
для ясности и сохрани результат в published/{{ change_file.Title }}.md.

Ключи, сверенные с рантаймом:

Ключ Значение
executor llm (по умолчанию) или code
model, tools какая модель и какие инструменты доступны роли
read_patterns, write_patterns glob-границы: рантайм проверяет каждое чтение и каждую запись
mode change, cron или both
trigger_include, trigger_exclude, trigger_on какие пути и события (create, update, remove) запускают роль
cron_schedule cron-выражение для ролей по расписанию
attach_notes glob заметок, которые предзагружаются в доставку как контекст
for_each fan-out: один запуск на каждый элемент changed_files или attached_notes
max_tokens, max_steps, timeout_seconds бюджет запуска (таймаут по умолчанию 300 с)
max_depth предел глубины каскада, защита от петель
concurrency skip, queue_one или allow_overlap, если доставки накапливаются
env_passthrough, env_prefix только для код-ролей: какие переменные окружения получает дочерний процесс

Флит валидирует каждую роль при обнаружении и вслух отказывается от плохих: роль объявила инструмент, которого у флита нет; change-роль без триггеров; код-роль без блока кода. Ошибка конфигурации всплывает при опросе, а не молча в три часа ночи.

Триггеры: по изменению и по расписанию

Триггеры по изменению — основной цикл: заметки запускают агентов. Сохраните заметку, совпадающую с trigger_include, — хаб отправит доставку через change-вебхук во флит. Флит проверит HMAC-подпись, отрендерит тело роли против контекста триггера и запустит агента. Записи одной роли могут совпадать с триггерами другой, поэтому роли выстраиваются в каскады. Счётчик depth и лимит max_depth не дают каскаду превратиться в петлю.

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

Вебхуки вручную не настраиваются. Цикл reconcile создаёт, обновляет и удаляет их на хабе так, чтобы они соответствовали найденным заметкам-ролям.

Инструменты и границы доступа

LLM-роль получает пять инструментов:

Инструмент Что делает
search(query) полнотекстовый поиск в пределах зоны чтения
read_note(path) прочитать заметку (только зона чтения)
write_note(path, content) создать или заменить заметку (только зона записи)
patch_note(path, find, replace) точечная замена; ошибка, если find найден не ровно один раз
finish(answer) завершить запуск с итогом

Каждый вызов проверяется по read_patterns и write_patterns на уровне рантайма, а не промпта. Запрос за границей отклоняется, и отказ возвращается модели. Записи идут через токен с ограниченным доступом, который хаб выпускает под одну доставку: агент физически не дотянется до чужих путей, а каждая запись — обычная версия заметки, которую можно посмотреть и откатить.

executor: code

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

---
executor: code
mode: cron
cron_schedule: "*/30 * * * *"
write_patterns: ["transcripts/**", "logs/**"]
env_passthrough: [KRISP_API_TOKEN]
---
```python
import json, os
# забрать новые записи и вывести изменения в stdout:
print(json.dumps({"changes": [
    {"path": "transcripts/2026-07-02_standup.md", "content": "..."}
]}))
```

Программа — первый блок кода в теле (python, bash или node). Контекст доставки приходит JSON-файлом, путь к которому лежит в $FLEET_INPUT; программа печатает в stdout объект {"changes": [...]}, и флит применяет каждое изменение через ту же проверку write_patterns, что и write_note. Запуск кода не обходит границы доступа.

Изоляция по умолчанию строгая. Дочерний процесс работает в песочнице на уровне ОС (Linux-неймспейсы плюс ограничение файловой системы через Landlock, сеть закрыта, пока оператор её не разрешит), и на неподдерживаемых системах песочница отказывает запуск, а не деградирует молча. Окружение вычищено: ни одна переменная не попадает в процесс, пока роль не перечислит её в env_passthrough или env_prefix. Сам запуск кода выключен, пока оператор флита не разрешит конкретные интерпретаторы флагом --allowed-programs.

Fan-out и шаблоны

Тело роли — Jet-шаблон, который рендерится на каждую доставку. Доступны четыре переменные: changed_files (заметки, вызвавшие доставку), change_file (текущая заметка при fan-out), attached_notes (контекст, предзагруженный через attach_notes) и depth. Секреты в шаблон не попадают, а обращение к неизвестной переменной останавливает доставку до любого вызова модели.

for_each: changed_files запускает агента по разу на каждую изменённую заметку, for_each: attached_notes — на каждую прикреплённую. Пакет из десяти правок превращается в десять сфокусированных запусков вместо одного запутанного, и сбой одного элемента не срывает остальные.

Бюджеты, расход и остановка

Каждый запуск ограничен тремя жёсткими лимитами: max_tokens, max_steps и timeout_seconds. Модель не может поднять ни один из них, а потолки оператора флита (--token-ceiling, по умолчанию 100 000; --step-ceiling, по умолчанию 25) режут запросы роли сверху: действует минимум из двух значений.

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

Останавливается флит аккуратно: по SIGTERM перестаёт принимать доставки, ждёт завершения текущих запусков до --shutdown-grace-seconds (по умолчанию 30) и снимает свои вебхуки. Для rolling-деплоев есть --keep-webhooks-on-shutdown: вебхуки остаются на месте, и хаб повторяет доставки, пока не поднимется новый экземпляр флита.

Рабочий пример: база знаний, которая собирает себя сама

Самый наглядный конвейер на флите превращает сырые транскрипты звонков в связанную базу знаний. Результат описан в Звонки в базу знаний; здесь тот же конвейер показан как набор ролей флита.

Krisp API
   │  cron: код-роль (инжест)
   ▼
transcripts/<дата>_<slug>.md    сырой транскрипт, дословно, никогда не правится
   │  change-вебхук на transcripts/** → роль сегментации
   ▼
segments/<id>.md                карта тем с таймкодами
   │  change-вебхук на segments/** → роль извлечения
   ▼
calls/, concepts/, log/, daily/  база знаний собирается сама

Каждая стадия — одна заметка-роль:

  1. Инжест — код-роль по расписанию. Забирает новые звонки из Krisp API и пишет сырые транскрипты. Это единственная стадия, привязанная к источнику: замените скрипт — и тот же конвейер обрабатывает субтитры YouTube или вывод бота для встреч.
  2. Сегментация — LLM-роль на transcripts/**. Читает транскрипт из тела доставки и пишет грубую карту тем с таймкодами.
  3. Извлечение — LLM-роль на segments/**. Пишет заметку-разбор, создаёт или дополняет заметки-концепты, добавляет чекбоксы задач в дневную заметку и дописывает тематические логи. Каждое утверждение цитирует сегмент транскрипта дословно, факты прослеживаются до источника.

Никто ничего не запускает руками. Транскрипт появился — каскад change-вебхуков делает остальное; max_depth ограничивает цепочку, а на выходные папки ничего не срабатывает, поэтому она завершается. Расход — около 0,15 $ на модельные вызовы за 50-минутный звонок. Конвейер живёт в github.com/trip2g/krisp_knowledge.

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

Как запустить флит

Флит собирается из репозитория trip2g отдельным бинарником:

go build -o fleet ./cmd/fleet

./fleet \
  --trip2g-url https://your-hub.example \
  --callback-url http://fleet-host:9090 \
  --jwt-secret  "<user-token secret хаба>" \
  --fleet-secret "<любой случайный seed>" \
  --llm-base-url https://openrouter.ai/api/v1 \
  --llm-api-key  "<ключ>" \
  --agents-folder roles/ \
  --allowed-programs python,bash

У каждого флага есть переменная окружения TRIP2G_FLEET_<ФЛАГ>. --jwt-secret — user-token secret хаба: через него флит сам заводит себе админскую учётку, без ручной возни с API-ключами. --llm-base-url принимает любой OpenAI-совместимый эндпоинт, включая локальную модель.

Два переключателя делают разработку ролей дешёвой:

  • --dry-run находит и валидирует все роли, печатает итоговую конфигурацию и выходит, ничего не регистрируя.
  • --once role.md --vault ./my-vault запускает одну роль против локальной папки без хаба и вебхуков: правка, запуск, проверка файлов, повтор.

Начните с одной роли, дешёвой модели и узких write_patterns. Расширяйте, когда запуски начнут выглядеть правильно.