Pipeline e automazione
Le pipeline sono la metà deterministica di Control Center. Un agente decide che cosa dire; una pipeline decide che cosa succede dopo, sempre nello stesso modo. Una pipeline è un DAG di passaggi che possono inviare agenti, eseguire script, creare un ramo in base a una condizione, eseguire un fan-out su una raccolta e passare dati tra un passaggio e l’altro: persistente, riprendibile e con costi tracciati.
Esistono per tre motivi: concatenare un processo ripetitivo in più passaggi, coordinare diversi agenti in sequenza o in parallelo e reagire a qualcosa che è accaduto senza che ci fosse una persona.
Modelli ed esecuzioni
Sezione intitolata “Modelli ed esecuzioni”Un modello (PipelineDefinition) è il DAG dichiarativo: passaggi, input
dichiarati e configurazione. Vive nel database dello spazio di lavoro stesso.
Un’esecuzione (PipelineRun) è una singola esecuzione di quel modello e
porta con sé un contenitore di stato modificabile, i totali di costo e token e
lo stato di ogni passaggio.
Un modello ha molte esecuzioni. Le esecuzioni sopravvivono a un riavvio: il motore ricarica le esecuzioni in corso e le riprende da dove si erano fermate.
Due meccanismi di salvaguardia fanno deliberatamente fallire l’intera esecuzione anziché riprendere un passaggio, perché riprendere sarebbe peggio che fermarsi:
- Un passaggio rimasto
suspendedpiù a lungo disuspendedStepTimeout(24 ore per impostazione predefinita) sta aspettando ticket che non raggiungeranno mai uno stato terminale, quindi viene fatto fallire per liberare l’esecuzione. L’eccezione èextras['awaitApproval'] == true, ovvero il gate di approvazione parzialeorchestration.awaitApproval.human.gatenon è esente. - Un passaggio dichiarato
extras: {'idempotent': false}che era in corso quando il server si è fermato potrebbe aver già applicato il proprio effetto collaterale (uno squash merge, la pulizia di un worktree). Rieseguirlo potrebbe applicarlo due volte, quindi il motore lo fa fallire e lascia a te il retry.
Il tipo pianifica, il corpo lavora
Sezione intitolata “Il tipo pianifica, il corpo lavora”Un passaggio ha due parti ortogonali, e tenerle separate è ciò che permette al motore di restare piccolo.
Il tipo (StepKind) dice al motore quando un passaggio diventa eseguibile
rispetto ai passaggi a monte: è l’unica cosa che lo scheduler deve sapere. Ce ne
sono sei: trigger, listen, join, router, forEach e terminal. Il
corpo (bodyKey) è il lavoro che il passaggio svolge una volta
pianificato. I corpi sono closure registrate per chiave all’avvio del server,
quindi molti passaggi di tipi diversi possono condividere un unico corpo: diversi
passaggi listen che eseguono tutti conversation.promptAgent, per esempio.
Poiché la semantica di pianificazione risiede in sei tipi, il catalogo dei comportamenti può crescere senza che nessuno tocchi lo scheduler. Vedi Passaggi delle pipeline per il catalogo completo di tipi, corpi e relativa configurazione.
Vale la pena conoscere due conseguenze di questo design basato su registry prima di scrivere un modello a mano:
- Un passaggio il cui
bodyKeynon è registrato produce un avviso al caricamento del modello e poi non viene mai eseguito. A runtime non fallisce nulla in modo evidente. I passaggitriggereterminalsono esenti da questo avviso: il trigger esegue comunque il corpo no-oppipeline.triggercosì i listener a valle si attivano; un passaggioterminalnon ha corpo ed è una sentinella dello scheduler. - Le chiavi nella mappa
extrasdi un nodo sono a corrispondenza esatta e in caso contrario vengono ignorate in silenzio. Soloextras['spaceId']colloca il lavoro di un passaggio agente in una conversazione esistente.extras['mode'](chat/plan/review) viene letto sul nodo che apre la stanza (messaging.createSpace, predefinitoreview): un passaggio agente successivo non può riassegnare i permessi di una conversazione a cui si è unito. Qualsiasi altra grafia dell’una o dell’altra chiave viene ignorata.
Stato e flusso di dati
Sezione intitolata “Stato e flusso di dati”I passaggi non si chiamano tra loro. Leggono e scrivono su un contenitore di
stato condiviso dell’esecuzione: un passaggio legge i propri input per chiave,
viene eseguito e riscrive il proprio output sotto il suo outputKey. Un
StateReducer unisce le scritture concorrenti quando rami paralleli puntano alla
stessa chiave, e un TemplateRenderer sostituisce i segnaposto {{...}} nei
prompt e negli script attingendo dallo stesso stato.
Ecco perché una pipeline è ispezionabile a posteriori: il contenitore di stato racconta tutta la storia dell’esecuzione.
Che cosa avvia un’esecuzione
Sezione intitolata “Che cosa avvia un’esecuzione”Le righe PipelineTrigger dichiarano quando un modello si avvia
automaticamente. Esistono quattro tipi:
| Trigger | Avvia un’esecuzione quando |
|---|---|
| Evento di dominio | Si attiva un evento di dominio corrispondente — per esempio PullRequestPublished o TicketAssigned |
| Pianificazione | Un’espressione cron/interval giunge a scadenza |
| Manuale | La esegui a mano; la presenza di un trigger manuale abilitato è ciò che inserisce il modello nel selettore di esecuzione |
| Webhook | Un sistema esterno invia una POST a /webhooks/<token> sulla porta HTTP del server |
I trigger di evento sono un insieme chiuso: il dispatcher conosce 16 tipi di
evento e li propone nel selettore (EventPayloadMapper.knownEventTypes), e
nient’altro può avviare una pipeline. Quattordici di questi 16 hanno una
mappatura del payload. Due — TicketCreated e TicketStatusChanged — non ne
hanno, quindi un trigger su uno dei due resta abilitato e non si attiva mai.
Vedi Eventi di dominio per
l’elenco completo.
Il token del webhook funge anche da segreto HMAC: l’header
X-Hub-Signature-256: sha256=<hex> della richiesta viene verificato rispetto a
HMAC-SHA256(body, token), le consegne non verificate vengono rifiutate e non
possono essere riprodotte, e i duplicati vengono scartati da dedupe_key.
Poiché le righe dei trigger sono univoche su (spazio di lavoro, tipo di evento,
modello), un modello può avere al massimo un trigger webhook e un trigger per
tipo di evento di dominio.
I modelli integrati inizializzano i propri trigger manuali e di evento come abilitati; i trigger pianificati come disabilitati, quindi una scansione ricorrente è sempre opt-in.
I modelli che arrivano disabilitati
Sezione intitolata “I modelli che arrivano disabilitati”Tredici modelli di pipeline vengono inizializzati in ogni spazio di lavoro e
otto di essi arrivano disabilitati: external_pr_welcome, cross_review,
ticket_to_pr, pr_triage, pre_merge_gate, release_notes, dep_audit e
pr_digest.
Questa è la parte che sorprende. I loro trigger vengono inizializzati come
abilitati, ma PipelineEngine.start rifiuta un modello disabilitato e
restituisce null: così il trigger si attiva a vuoto e non compare alcuna
esecuzione. L’interruttore è il modello, non il trigger. Abilitalo in
Impostazioni → Spazio di lavoro → Modelli di pipeline prima di aspettarti che uno
di quegli otto faccia qualcosa.
Re-inizializzare uno spazio di lavoro conserva le tue scelte: l’impostazione
isEnabled e le righe dei trigger esistenti sopravvivono. C’è esattamente una
deroga deliberata: la pianificazione giornaliera di pr_merged_cleanup viene
forzata ad abilitata anche se in precedenza era disattivata, perché si tratta di
garbage collection. Senza di essa, le righe dei worktree, le partizioni del grafo
del codice e le copie copy-on-write si accumulano, e non è mai stato pensato come
una scelta dell’utente.
Un’altra asimmetria da conoscere: il modello integrato pr_review di proposito
non pubblica più su GitHub. La pubblicazione di una revisione è un passaggio
separato, soggetto a controllo dell’utente, e una pipeline che pubblicasse
direttamente lo farebbe due volte. cross_review, ticket_to_pr e pr_triage,
invece, pubblicano ancora un commento direttamente su GitHub quando vengono
eseguiti.
Che cosa sa il motore
Sezione intitolata “Che cosa sa il motore”PipelineEngine crea il record dell’esecuzione, pianifica il nodo di avvio
corrispondente al tipo di evento dell’esecuzione e poi percorre il DAG: man mano
che i passaggi si completano, valuta i trigger a valle, risolve i rami del
router, attende i join, applica le policy di retry e il continue-on-fail e
persiste stato, costo ed errori a ogni passaggio. Gli archi che partono da un
altro avvio vengono ignorati per quell’esecuzione, quindi un’attivazione
pianificata non attende una condizione solo manuale.
Ciò che non sa è qualsiasi cosa riguardante i ticket, l’invio, GitHub o la
messaggistica. Il suo unico punto di estensione è PipelineBodyRegistry. I corpi
registrati lì sono ciò che contiene AgentDispatchPort, MessagingPort e i loro
fratelli: ecco perché aggiungere una capacità alle pipeline non significa mai
modificare lo scheduler.
Gestione degli errori
Sezione intitolata “Gestione degli errori”Un passaggio che fallisce registra il proprio messaggio di errore e passa a
failed. Se il passaggio dichiara continueOnFail, i passaggi a valle
proseguono e l’errore viene archiviato nello stato dell’esecuzione; altrimenti
l’esecuzione fallisce. Un passaggio con una StepRetryPolicy viene ritentato per
prima, con backoff lineare o esponenziale, prima che si applichi l’una o l’altra
di queste regole.
Vedi anche
Sezione intitolata “Vedi anche”- Eventi di dominio: che cosa ascolta un trigger di evento
- Orchestrazione: un piano che approvi, compilato in una di queste pipeline
- Ticket e delega: che cosa creano e completano i passaggi delle pipeline
- Passaggi delle pipeline: il catalogo completo di tipi, corpi e configurazione
- Crea la tua prima pipeline
- Crea un modello di pipeline
- Esegui una pipeline manualmente
- Monitora le esecuzioni delle pipeline
- Configura i trigger delle pipeline