Русский
Флит: агентский сайдкар
Флит — небольшой демон, который работает рядом с хабом trip2g и превращает заметки в агентов. Вы пишете заметку-роль: фронтматтер задаёт, когда запускаться и что агенту можно трогать, тело — инструкция. Положили заметку в папку агентов — агент существует. Изменили подходящую заметку в хранилище — агент запустился. Удалили заметку-роль — агента больше нет. Ни конфигов пайплайнов, ни YAML-репозитория, ни редеплоев: хранилище и есть панель управления.
Если сначала нужна теория — что такое цикл инструментов LLM и почему границы доступа должен проверять рантайм, а не промпт, — читайте Как работают LLM-агенты. Эта страница о практике: что умеет флит и как на нём собран настоящий конвейер.
В статье:
- Что делает флит
- Роли — это заметки
- Триггеры: по изменению и по расписанию
- Инструменты и границы доступа
- executor: code
- Fan-out и шаблоны
- Бюджеты, расход и остановка
- Рабочий пример: база знаний, которая собирает себя сама
- Как запустить флит
Что делает флит
Флит — сайдкар: отдельный процесс (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/ база знаний собирается сама
Каждая стадия — одна заметка-роль:
- Инжест — код-роль по расписанию. Забирает новые звонки из Krisp API и пишет сырые транскрипты. Это единственная стадия, привязанная к источнику: замените скрипт — и тот же конвейер обрабатывает субтитры YouTube или вывод бота для встреч.
- Сегментация — LLM-роль на
transcripts/**. Читает транскрипт из тела доставки и пишет грубую карту тем с таймкодами. - Извлечение — 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. Расширяйте, когда запуски начнут выглядеть правильно.