Zum Inhalt springen

Pipelines und Automatisierung

Pipelines sind die deterministische Hälfte von Control Center. Ein Agent entscheidet, was er sagt; eine Pipeline entscheidet, was als Nächstes geschieht, jedes Mal auf dieselbe Weise. Eine Pipeline ist ein DAG aus Schritten, die Agenten losschicken, Skripte ausführen, an einer Bedingung verzweigen, über eine Sammlung auffächern und Daten zwischen den Schritten weiterreichen — gespeichert, fortsetzbar und mit Kosten erfasst.

Es gibt sie aus drei Gründen: einen wiederkehrenden Ablauf aus mehreren Schritten zu verketten, mehrere Agenten nacheinander oder parallel zu koordinieren und auf etwas zu reagieren, das geschehen ist, ohne dass ein Mensch dabei ist.

Eine Vorlage (PipelineDefinition) ist der deklarative DAG: Schritte, deklarierte Eingaben und Konfiguration. Sie liegt in der eigenen Datenbank des Arbeitsbereichs. Ein Lauf (PipelineRun) ist eine Ausführung dieser Vorlage und trägt eine veränderliche Zustandsmap, die Summen für Kosten und Token sowie den Status je Schritt.

Eine Vorlage hat viele Läufe. Läufe überstehen einen Neustart: Die Engine lädt noch nicht abgeschlossene Läufe neu und setzt dort an, wo sie stehen geblieben sind.

Zwei Sicherungen lassen den ganzen Lauf absichtlich scheitern, statt einen Schritt fortzusetzen, weil Fortsetzen schlimmer wäre als Anhalten:

  • Ein Schritt, der länger als suspendedStepTimeout (standardmäßig 24 Stunden) suspended war, wartet auf Tickets, die nie einen Endzustand erreichen, und wird deshalb fehlgeschlagen, damit der Lauf frei wird. Die Ausnahme ist extras['awaitApproval'] == true — das ist das Tor für die teilweise Freigabe orchestration.awaitApproval. human.gate ist davon nicht ausgenommen.
  • Ein Schritt mit extras: {'idempotent': false}, der mitten in der Ausführung war, als der Server anhielt, kann seinen Seiteneffekt schon angewandt haben (ein Squash-Merge, ein Aufräumen des Worktrees). Ihn noch einmal auszuführen könnte den Effekt doppelt anwenden, also lässt die Engine ihn scheitern und überlässt den erneuten Versuch dir.

Ein Schritt hat zwei orthogonale Teile, und sie getrennt zu halten hält die Engine klein.

Die Art (StepKind) sagt der Engine, wann ein Schritt relativ zu seinen vorgelagerten Schritten lauffähig wird — das ist das Einzige, was der Scheduler wissen muss. Es gibt sechs: trigger, listen, join, router, forEach und terminal. Der Body (bodyKey) ist die Arbeit, die der Schritt ausführt, sobald er eingeplant ist. Bodies sind Closures, die beim Start des Servers unter einem Schlüssel registriert werden, sodass viele Schritte verschiedener Arten denselben Body teilen können: mehrere listen-Schritte, die alle conversation.promptAgent ausführen, zum Beispiel.

Weil die Semantik des Einplanens in sechs Arten steckt, kann der Katalog der Verhaltensweisen wachsen, ohne dass jemand den Scheduler anfasst. Der vollständige Katalog der Arten, Bodies und ihrer Konfiguration steht unter Pipeline-Schrittarten.

Zwei Folgen des Registry-Entwurfs solltest du kennen, bevor du eine Vorlage von Hand schreibst:

  • Ein Schritt, dessen bodyKey nicht registriert ist, erzeugt eine Warnung beim Laden der Vorlage und wird danach nie ausgeführt. Zur Laufzeit scheitert nichts laut. Schritte der Art trigger und terminal sind von dieser Warnung ausgenommen: Der Trigger führt trotzdem den No-op-Body pipeline.trigger aus, damit nachgelagerte Listener auslösen; ein terminal-Schritt hat keinen Body und ist eine Sentinel des Schedulers.
  • Schlüssel in der extras-Map eines Knotens gelten nur bei exaktem Treffer und werden sonst still ignoriert. Nur extras['spaceId'] legt die Arbeit eines Agenten-Schritts in einen bestehenden Bereich. extras['mode'] (chat / plan / review) wird auf dem Knoten gelesen, der den Bereich öffnet (messaging.createSpace, Standard review) — ein späterer Agenten-Schritt kann einen Bereich, dem er beigetreten ist, nicht neu berechtigen. Jede andere Schreibweise eines der beiden Schlüssel wird ignoriert.

Schritte rufen einander nicht auf. Sie lesen aus einer gemeinsamen Zustandsmap auf dem Lauf und schreiben hinein: Ein Schritt liest seine Eingaben über den Schlüssel, führt aus und schreibt seine Ausgabe unter dem eigenen outputKey zurück. Ein StateReducer führt gleichzeitige Schreibvorgänge zusammen, wenn parallele Zweige denselben Schlüssel treffen, und ein TemplateRenderer ersetzt Platzhalter {{...}} in Prompts und Skripten aus ebendiesem Zustand.

Deshalb lässt sich eine Pipeline hinterher nachvollziehen — die Zustandsmap ist die ganze Geschichte des Laufs.

PipelineTrigger-Zeilen legen fest, wann eine Vorlage von selbst startet. Es gibt vier Arten:

Trigger Startet einen Lauf, wenn
Domain-Ereignis Ein passendes Domain-Ereignis eintritt — zum Beispiel PullRequestPublished oder TicketAssigned
Zeitplan Ein Cron- oder Intervallausdruck fällig wird
Manuell Du ihn von Hand startest; ein aktivierter manueller Trigger legt die Vorlage in „Pipeline ausführen“
Webhook Ein externes System per POST an /webhooks/<token> auf dem HTTP-Port des Servers sendet

Ereignis-Trigger sind eine geschlossene Menge: 16 Ereignistypen kennt der Dispatcher, und genau die bietet die Auswahl an (EventPayloadMapper.knownEventTypes). Nichts anderes kann eine Pipeline starten. Vierzehn der 16 haben eine Payload-Zuordnung. Zwei — TicketCreated und TicketStatusChanged — haben keine, ein Trigger auf einen der beiden bleibt also aktiviert und löst nie aus. Die vollständige Liste steht unter Domain-Ereignisse.

Das Webhook-Token ist zugleich das HMAC-Geheimnis: Der Header X-Hub-Signature-256: sha256=<hex> der Anfrage wird gegen HMAC-SHA256(body, token) geprüft, nicht verifizierte Zustellungen werden abgelehnt und lassen sich nicht wiederholen, und Duplikate verwirft dedupe_key. Weil Trigger-Zeilen auf (Arbeitsbereich, Ereignistyp, Vorlage) eindeutig sind, kann eine Vorlage höchstens einen Webhook-Trigger und einen Trigger je Domain-Ereignistyp haben.

Die mitgelieferten Vorlagen legen ihre manuellen und Ereignis-Trigger aktiviert an; geplante Trigger werden deaktiviert angelegt, ein wiederkehrender Durchlauf ist also immer eine bewusste Wahl.

In jeden Arbeitsbereich werden dreizehn Pipeline-Vorlagen angelegt, und acht davon werden deaktiviert ausgeliefert: external_pr_welcome, cross_review, ticket_to_pr, pr_triage, pre_merge_gate, release_notes, dep_audit und pr_digest.

Genau das überrascht. Ihre Trigger werden aktiviert angelegt, aber PipelineEngine.start lehnt eine deaktivierte Vorlage ab und gibt null zurück — der Trigger läuft also ins Leere, und es erscheint kein Lauf. Der Schalter ist die Vorlage, nicht der Trigger. Aktiviere sie unter Einstellungen → Arbeitsbereich → Pipeline-Vorlagen, bevor du von einer der acht etwas erwartest.

Ein erneutes Seeding eines Arbeitsbereichs behält deine Wahl: die Einstellung isEnabled und die bestehenden Trigger-Zeilen bleiben. Es gibt genau eine gewollte Ausnahme — der tägliche Zeitplan von pr_merged_cleanup wird zwingend aktiviert, auch wenn er vorher aus war, weil das die Bereinigung ist. Ohne sie häufen sich Worktree-Zeilen, Partitionen des Code-Graph und Copy-on-Write-Kopien an, und das Abschalten war nie als Wahl für dich gedacht.

Noch eine Asymmetrie: Die mitgelieferte pr_review postet absichtlich nicht mehr nach GitHub. Eine Review zu veröffentlichen ist ein eigener Schritt, den du freigibst, und eine Pipeline, die direkt postete, würde doppelt veröffentlichen. cross_review, ticket_to_pr und pr_triage posten bei einem Lauf weiterhin einen Kommentar direkt nach GitHub.

PipelineEngine legt den Laufdatensatz an, plant den Startknoten ein, der zum Ereignistyp des Laufs passt, und geht dann den DAG ab: Sobald Schritte fertig werden, wertet sie nachgelagerte Trigger aus, löst Router-Zweige auf, wartet auf Joins, wendet Wiederholungsrichtlinien und das Weiterlaufen bei Fehler an und speichert bei jedem Schritt Zustand, Kosten und Fehler. Kanten, die von einem anderen Start ausgehen, ignoriert sie für diesen Lauf, ein Start per Zeitplan wartet also nicht auf eine Bedingung nur für den manuellen Start.

Von Tickets, Dispatch, GitHub und Nachrichten weiß sie nichts. Ihre einzige Naht zur Erweiterung ist die PipelineBodyRegistry. Die dort registrierten Bodies halten AgentDispatchPort, MessagingPort und ihre Geschwister. Eine Fähigkeit zu den Pipelines hinzuzufügen heißt deshalb nie, den Scheduler zu ändern.

Ein Schritt, der scheitert, speichert seine Fehlermeldung und wechselt auf failed. Deklariert der Schritt continueOnFail, laufen die nachgelagerten Schritte weiter und der Fehler liegt im Zustand des Laufs; sonst scheitert der Lauf. Ein Schritt mit einer StepRetryPolicy wird zuerst wiederholt, mit linearem oder exponentiellem Backoff, bevor eines von beidem greift.