Przejdź do głównej zawartości

Potoki i automatyzacja

Potoki to deterministyczna połowa Control Center. Agent decyduje, co powiedzieć; potok decyduje, co stanie się dalej — za każdym razem tak samo. Potok to DAG kroków, które potrafią wysyłać agentów, uruchamiać skrypty, rozgałęziać się na warunku, rozdzielać pracę na elementy kolekcji i przekazywać dane między krokami — utrwalony, wznawialny i rozliczany pod względem kosztów.

Istnieją z trzech powodów: aby połączyć w łańcuch powtarzalny wieloetapowy proces, aby skoordynować kilku agentów po kolei lub równolegle oraz aby zareagować na coś, co się wydarzyło, bez obecności człowieka.

Szablon (PipelineDefinition) to deklaratywny DAG: kroki, zadeklarowane dane wejściowe i konfiguracja. Mieszka we własnej bazie danych obszaru roboczego. Uruchomienie (PipelineRun) to jedno wykonanie tego szablonu, niosące zmienny worek stanu, sumy kosztów i tokenów oraz status każdego kroku.

Jeden szablon ma wiele uruchomień. Uruchomienia przeżywają restart: silnik wczytuje ponownie uruchomienia w toku i podejmuje je od miejsca zatrzymania.

Dwa zabezpieczenia celowo kończą awarią całe uruchomienie zamiast wznawiać krok, bo wznowienie byłoby gorsze niż zatrzymanie:

  • Krok pozostający w stanie suspended dłużej niż suspendedStepTimeout (domyślnie 24 godziny) czeka na zgłoszenia, które nigdy nie osiągną stanu końcowego, więc kończy się awarią, aby uwolnić uruchomienie. Wyjątkiem jest extras['awaitApproval'] == true — to brama częściowego zatwierdzenia orchestration.awaitApproval. human.gate nie jest z niego zwolniony.
  • Krok zadeklarowany jako extras: {'idempotent': false}, który był w trakcie działania, gdy serwer się zatrzymał, mógł już zastosować swój efekt uboczny (scalanie squash, czyszczenie worktree). Ponowne uruchomienie mogłoby zastosować go dwa razy, więc silnik kończy go awarią, a ponowienie pozostawia do wykonania ręcznie.

Rodzaj decyduje o harmonogramie, ciało wykonuje pracę

Dział zatytułowany „Rodzaj decyduje o harmonogramie, ciało wykonuje pracę”

Krok ma dwie ortogonalne części i to ich rozdzielenie pozwala silnikowi pozostać małym.

Rodzaj (StepKind) mówi silnikowi, kiedy krok staje się gotowy do uruchomienia względem kroków powyżej — to jedyna informacja, jakiej potrzebuje scheduler. Jest ich sześć: trigger, listen, join, router, forEach i terminal. Ciało (bodyKey) to praca, którą krok wykonuje po zaplanowaniu. Ciała to domknięcia rejestrowane pod kluczem przy starcie serwera, więc wiele kroków różnych rodzajów może współdzielić jedno ciało: na przykład kilka kroków listen uruchamiających conversation.promptAgent.

Ponieważ semantyka planowania mieści się w sześciu rodzajach, katalog zachowań może rosnąć, a nikt nie musi przy tym ruszać schedulera. Pełny katalog rodzajów, ciał i ich konfiguracji znajdziesz w Rodzajach kroków potoku.

Dwie konsekwencje konstrukcji opartej na rejestrze warto znać, zanim napisze się szablon ręcznie:

  • Krok, którego bodyKey nie został zarejestrowany, generuje ostrzeżenie przy wczytywaniu szablonu, a potem nigdy się nie wykonuje. W czasie działania nic nie zgłasza głośno błędu. Kroki trigger i terminal są z tego ostrzeżenia zwolnione: wyzwalacz nadal uruchamia puste ciało pipeline.trigger, aby zadziałały kroki nasłuchujące poniżej; krok terminal nie ma ciała i jest wartownikiem schedulera.
  • Klucze w mapie extras węzła liczą się tylko przy dokładnym dopasowaniu, a w innym wypadku są po cichu ignorowane. Tylko extras['spaceId'] umieszcza pracę kroku agenta w istniejącej rozmowie. extras['mode'] (chat / plan / review) jest odczytywany na węźle, który otwiera przestrzeń (messaging.createSpace, domyślnie review) — późniejszy krok agenta nie może zmienić uprawnień rozmowy, do której dołączył. Każda inna pisownia któregokolwiek z tych kluczy jest ignorowana.

Kroki nie wywołują się nawzajem. Czytają ze wspólnego worka stanu uruchomienia i do niego zapisują: krok odczytuje swoje dane wejściowe po kluczu, wykonuje się i zapisuje wynik z powrotem pod własnym outputKey. StateReducer scala równoległe zapisy, gdy równoległe gałęzie celują w ten sam klucz, a TemplateRenderer podstawia w promptach i skryptach symbole zastępcze {{...}} z tego samego stanu.

Dlatego potok można obejrzeć po fakcie — worek stanu to cała historia uruchomienia.

Wiersze PipelineTrigger deklarują, kiedy szablon uruchamia się automatycznie. Są cztery rodzaje:

Wyzwalacz Uruchamia potok, gdy
Zdarzenie domenowe Zadziała pasujące zdarzenie domenowe — na przykład PullRequestPublished lub TicketAssigned
Harmonogram Nadejdzie termin wyrażenia cron lub przedziału czasowego
Ręczny Uruchamiasz go z ręki; to obecność włączonego wyzwalacza ręcznego sprawia, że szablon pojawia się w selektorze uruchomień
Webhook Zewnętrzny system wyśle żądanie POST do /webhooks/<token> na porcie HTTP serwera

Wyzwalacze zdarzeniowe to zbiór zamknięty: dispatcher zna 16 typów zdarzeń i oferuje je w selektorze (EventPayloadMapper.knownEventTypes), a nic innego nie może uruchomić potoku. Czternaście z tych 16 ma mapowanie ładunku. Dwa — TicketCreated i TicketStatusChanged — nie mają go wcale, więc wyzwalacz na którymkolwiek z nich pozostaje włączony i nigdy nie zadziała. Pełną listę znajdziesz w Zdarzeniach domenowych.

Token webhooka służy też jako sekret HMAC: nagłówek żądania X-Hub-Signature-256: sha256=<hex> jest weryfikowany względem HMAC-SHA256(body, token), niezweryfikowane dostawy są odrzucane i nie można ich odtworzyć, a duplikaty odrzuca dedupe_key. Ponieważ wiersze wyzwalaczy są unikalne względem (obszar roboczy, typ zdarzenia, szablon), szablon może mieć najwyżej jeden wyzwalacz webhooka i po jednym wyzwalaczu na typ zdarzenia domenowego.

Wbudowane szablony zasiewają swoje wyzwalacze ręczne i zdarzeniowe jako włączone; wyzwalacze harmonogramu są zasiewane jako wyłączone, więc cykliczne przemiatanie nigdy nie włącza się samo.

Do każdego obszaru roboczego zasiewa się trzynaście szablonów potoków, a osiem z nich jest domyślnie wyłączonych: external_pr_welcome, cross_review, ticket_to_pr, pr_triage, pre_merge_gate, release_notes, dep_audit i pr_digest.

Właśnie to zaskakuje ludzi. Ich wyzwalacze są zasiewane jako włączone, ale PipelineEngine.start odrzuca wyłączony szablon i zwraca null — więc wyzwalacz odpala w pustkę i żadne uruchomienie się nie pojawia. Przełącznikiem jest szablon, nie wyzwalacz. Włącz go w Ustawienia → Obszar roboczy → Szablony potoków, zanim zacznie się oczekiwać, że któreś z tych ośmiu cokolwiek zrobi.

Ponowne zasianie obszaru roboczego zachowuje wcześniejsze wybory: ustawienie isEnabled i istniejące wiersze wyzwalaczy przetrwają. Jest dokładnie jeden celowy wyjątek — dzienny harmonogram pr_merged_cleanup jest wymuszony jako włączony, nawet jeśli wcześniej był wyłączony, bo pełni rolę odśmiecania. Bez niego narastają wiersze worktree, partycje grafu kodu i kopie copy-on-write, a to nigdy nie miało być wyborem użytkownika.

Jeszcze jedna asymetria warta poznania: wbudowany pr_review celowo nie publikuje już na GitHubie. Opublikowanie recenzji to osobny krok wymagający zgody użytkownika, a potok, który publikowałby bezpośrednio, publikowałby podwójnie. Natomiast cross_review, ticket_to_pr i pr_triage nadal wysyłają komentarz prosto na GitHuba, gdy się uruchamiają.

PipelineEngine tworzy rekord uruchomienia, planuje węzeł startowy pasujący do typu zdarzenia tego uruchomienia, a następnie przechodzi DAG: gdy kroki się kończą, ocenia wyzwalacze poniżej, rozwiązuje gałęzie routera, czeka na złączenia, stosuje polityki ponawiania i kontynuację po błędzie oraz utrwala stan, koszt i błędy przy każdym kroku. Krawędzie wychodzące z innego startu są dla tego uruchomienia ignorowane, więc odpalenie z harmonogramu nie czeka na warunek dostępny wyłącznie ręcznie.

Czego natomiast nie wie: niczego o zgłoszeniach, wysyłaniu, GitHubie czy wiadomościach. Jedynym szwem rozszerzeń jest PipelineBodyRegistry. To właśnie zarejestrowane tam ciała przechowują AgentDispatchPort, MessagingPort i im pokrewne — dlatego dodanie potokom nowej możliwości nigdy nie oznacza zmiany schedulera.

Krok, który zawiedzie, zapisuje swój komunikat błędu i przechodzi do stanu failed. Jeśli krok deklaruje continueOnFail, kroki poniżej idą dalej, a błąd trafia do stanu uruchomienia; w przeciwnym razie całe uruchomienie kończy się awarią. Krok z StepRetryPolicy jest najpierw ponawiany, z liniowym lub wykładniczym opóźnieniem, zanim zadziała którykolwiek z tych mechanizmów.