Перейти к содержимому

Конвейеры и автоматизация

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

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

Шаблон (PipelineDefinition) — это декларативный DAG: шаги, объявленные входные данные и конфигурация. Он хранится в собственной базе данных рабочего пространства. Запуск (PipelineRun) — это одно выполнение этого шаблона, которое несёт изменяемый мешок состояния, суммарные затраты и токены, а также статус каждого шага.

У одного шаблона много запусков. Запуски переживают перезапуск: движок заново загружает выполняющиеся запуски и продолжает их с того места, где они остановились.

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

  • Шаг, который находится в состоянии suspended дольше suspendedStepTimeout (по умолчанию 24 часа), ждёт тикеты, которые никогда не достигнут терминального состояния, поэтому его проваливают, чтобы освободить запуск. Исключение — extras['awaitApproval'] == true, то есть шлюз частичного подтверждения orchestration.awaitApproval. Для human.gate исключения нет.
  • Шаг, объявленный как extras: {'idempotent': false} и находившийся в процессе выполнения, когда сервер остановился, мог уже применить свой побочный эффект (squash-merge, очистку рабочего дерева). Повторный запуск мог бы применить его дважды, поэтому движок проваливает шаг, а повтор оставляет на ваше усмотрение.

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

Вид (StepKind) сообщает движку, когда шаг становится готовым к выполнению относительно вышестоящих шагов, — это единственное, что нужно знать планировщику. Видов шесть: trigger, listen, join, router, forEach и terminal. Тело (bodyKey) — это работа, которую шаг выполняет после того, как его запланировали. Тела — это замыкания, регистрируемые по ключу при старте сервера, поэтому множество шагов разных видов могут использовать одно тело: например, несколько шагов listen, все выполняющие conversation.promptAgent.

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

Два следствия устройства реестра стоит знать, прежде чем писать шаблон вручную:

  • Шаг, чей bodyKey не зарегистрирован, выдаёт предупреждение при загрузке шаблона и затем никогда не выполняется. Во время работы ничего не падает с громкой ошибкой. Шаги trigger и terminal от этого предупреждения освобождены: триггер всё равно выполняет тело pipeline.trigger, которое ничего не делает, чтобы сработали нижестоящие слушатели; у шага terminal тела нет, это сторожевой элемент планировщика.
  • Ключи в карте extras узла сопоставляются только точно, а в остальных случаях молча игнорируются. Только extras['spaceId'] помещает работу агентского шага в существующий разговор. extras['mode'] (chat / plan / review) читается на узле, который открывает комнату (messaging.createSpace, по умолчанию review), — более поздний агентский шаг не может заново выдать права разговору, к которому присоединился. Любое другое написание любого из этих ключей игнорируется.

Шаги не вызывают друг друга. Они читают и пишут в общий мешок состояния запуска: шаг читает свои входные данные по ключу, выполняется и записывает результат обратно под своим outputKey. StateReducer сливает конкурентные записи, когда параллельные ветви нацелены на один и тот же ключ, а TemplateRenderer подставляет заполнители {{...}} в промпты и скрипты из этого же состояния.

Именно поэтому конвейер можно разобрать постфактум — мешок состояния и есть вся история запуска.

Строки PipelineTrigger объявляют, когда шаблон запускается автоматически. Видов четыре:

Триггер Инициирует запуск, когда
Доменное событие Срабатывает подходящее доменное событие — например, PullRequestPublished или TicketAssigned
Расписание Наступает срок выражения cron или интервала
Вручную Вы запускаете его сами; наличие включённого ручного триггера делает шаблон доступным на странице «Запустить конвейер»
Webhook Внешняя система отправляет POST на /webhooks/<token> на HTTP-порт сервера

Событийные триггеры — закрытый набор: диспетчер знает 16 типов событий, и именно они доступны для выбора (EventPayloadMapper.knownEventTypes); ничто другое не может запустить конвейер. У четырнадцати из этих 16 есть сопоставление payload. У двух — TicketCreated и TicketStatusChanged — его нет, поэтому триггер на любое из них остаётся включённым и никогда не срабатывает. Полный список — в разделе Доменные события.

Токен webhook заодно служит секретом HMAC: заголовок запроса X-Hub-Signature-256: sha256=<hex> проверяется по HMAC-SHA256(body, token), непроверенные доставки отклоняются и не могут быть воспроизведены, а дубликаты отбрасываются по dedupe_key. Поскольку строки триггеров уникальны по (рабочее пространство, тип события, шаблон), у шаблона может быть не более одного webhook-триггера и по одному триггеру на каждый тип доменного события.

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

Шаблоны, которые поставляются отключёнными

Заголовок раздела «Шаблоны, которые поставляются отключёнными»

В каждое рабочее пространство добавляются тринадцать шаблонов конвейеров, и восемь из них поставляются отключёнными: external_pr_welcome, cross_review, ticket_to_pr, pr_triage, pre_merge_gate, release_notes, dep_audit и pr_digest.

Именно это и удивляет людей. Их триггеры создаются включёнными, но PipelineEngine.start отказывается запускать отключённый шаблон и возвращает null — поэтому триггер срабатывает в пустоту, и никакой запуск не появляется. Переключатель — это шаблон, а не триггер. Включите его в разделе Настройки → Рабочее пространство → Шаблоны конвейеров, прежде чем ждать от этих восьми хоть какой-то работы.

Повторное создание шаблонов в рабочем пространстве сохраняет всё, что вы выбрали: ваша настройка isEnabled и существующие строки триггеров остаются на месте. Есть ровно одно намеренное исключение: ежедневное расписание pr_merged_cleanup принудительно включается, даже если раньше было выключено, потому что это сборка мусора. Без неё накапливались бы строки рабочих деревьев, разделы графа кода и копии copy-on-write, а это никогда не задумывалось как выбор пользователя.

Ещё одна асимметрия, которую стоит знать: встроенный pr_review намеренно больше не публикует ничего в GitHub. Публикация проверки — отдельный шаг, требующий подтверждения пользователя, а конвейер, публикующий напрямую, публиковал бы всё дважды. А вот cross_review, ticket_to_pr и pr_triage при запуске по-прежнему отправляют комментарий прямо в GitHub.

PipelineEngine создаёт запись о запуске, планирует стартовый узел, соответствующий типу события запуска, и затем обходит DAG: по мере завершения шагов он вычисляет нижестоящие триггеры, разрешает ветвления router, дожидается join, применяет политики повторов и продолжение при сбое, а также сохраняет состояние, стоимость и ошибки на каждом шаге. Рёбра, ведущие из другого старта, для этого запуска игнорируются, поэтому срабатывание по расписанию не ждёт условия, доступного только при ручном запуске.

Чего он не знает — так это ничего о тикетах, диспетчеризации, GitHub или обмене сообщениями. Его единственная точка расширения — PipelineBodyRegistry. Именно зарегистрированные там тела содержат AgentDispatchPort, MessagingPort и их родственников, поэтому добавление новой возможности в конвейеры никогда не означает правку планировщика.

Упавший шаг записывает своё сообщение об ошибке и переходит в failed. Если шаг объявляет continueOnFail, нижестоящие шаги продолжают работу, а ошибка складывается в состояние запуска; иначе запуск завершается ошибкой. Шаг с StepRetryPolicy сначала повторяется — с линейной или экспоненциальной задержкой, — и только потом применяется любой из этих вариантов.