Перейти до вмісту

Конвеєри та автоматизація

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

Вони існують із трьох причин: щоб об’єднувати в ланцюжок повторюваний багатокроковий процес, щоб координувати кілька агентів послідовно чи паралельно та щоб реагувати на подію, яка сталася без участі людини.

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

Один шаблон має багато запусків. Запуски переживають перезапуск: рушій перезавантажує запуски в процесі виконання й підхоплює їх там, де вони зупинилися.

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

  • Крок, який перебував у стані suspended довше за suspendedStepTimeout (типово 24 години), чекає на тікети, що ніколи не досягнуть кінцевого стану, тож його позначають як невдалий, щоб звільнити запуск. Виняток — extras['awaitApproval'] == true — це шлюз часткового схвалення orchestration.awaitApproval. human.gate під виняток не підпадає.
  • Крок, оголошений як extras: {'idempotent': false}, який був у процесі виконання, коли сервер зупинився, міг уже застосувати свій побічний ефект (squash-злиття, прибирання worktree). Повторний запуск міг би застосувати його двічі, тож рушій провалює його й залишає повторну спробу вам.

Крок має дві ортогональні частини, і саме те, що їх тримають окремо, дає змогу рушію залишатися малим.

Тип (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/інтервалу
Вручну Ви запускаєте його власноруч; саме наявність увімкненого ручного тригера додає шаблон у селектор запуску
Вебхук Зовнішня система надсилає POST на /webhooks/<token> на HTTP-порті сервера

Тригери подій — це закрита множина: диспетчер знає 16 типів подій і пропонує їх у селекторі (EventPayloadMapper.knownEventTypes), і ніщо інше не може запустити конвеєр. Чотирнадцять із цих 16 мають зіставлення корисного навантаження. Два — TicketCreated і TicketStatusChanged — не мають жодного, тож тригер на будь-який із них залишається увімкненим і ніколи не спрацьовує. Дивіться Доменні події, щоб побачити повний список.

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

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

Тринадцять шаблонів конвеєрів засіваються в кожен робочий простір, і вісім із них постачаються вимкненими: 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 примусово вмикається, навіть якщо раніше був вимкнений, бо це збирання сміття. Без нього рядки worktree, розділи графа коду та копії copy-on-write накопичуються, а це ніколи не задумувалося як користувацький вибір.

Ще одна асиметрія, яку варто знати: вбудований pr_review навмисно більше не публікує на GitHub. Публікація перевірки — це окремий крок, який контролює користувач, а конвеєр, що публікував би напряму, опублікував би двічі. cross_review, ticket_to_pr і pr_triage усе ще публікують коментар напряму в GitHub, коли виконуються.

PipelineEngine створює запис запуску, планує початковий вузол, що відповідає типу події запуску, а потім обходить DAG: у міру завершення кроків він оцінює тригери нижче за потоком, розв’язує гілки router, чекає на join, застосовує політики повторних спроб і continue-on-fail та зберігає стан, вартість і помилки на кожному кроці. Ребра, що виходять з іншого старту, для цього запуску ігноруються, тож спрацьовування розкладу не чекає на умову, доступну лише вручну.

Чого він не знає — це будь-чого про тікети, диспетчеризацію, GitHub чи обмін повідомленнями. Єдиний шов для розширення — це PipelineBodyRegistry. Саме тіла, зареєстровані там, тримають AgentDispatchPort, MessagingPort та їхніх супутників — тому додавання можливості до конвеєрів ніколи не означає зміни планувальника.

Крок, що падає, записує своє повідомлення про помилку й переходить у стан failed. Якщо крок оголошує continueOnFail, наступні кроки продовжують виконання, а помилка зберігається в стані запуску; інакше запуск завершується невдачею. Крок із StepRetryPolicy спершу повторюється — з лінійною або експоненційною затримкою, — перш ніж застосується будь-що з наведеного вище.