Vai al contenuto

Worker persistenti

Un terminale interattivo o una chat non restano vivi per sempre. Un worker persistente gira sotto il gestore dei servizi dell'host, interroga Griglia e avvia una nuova sessione non interattiva dell'agente ogni volta che c'è del lavoro assegnato. Chiudere il terminale, il browser o la sessione originale dell'agente non lo ferma.

Griglia distribuisce il worker e un template di servizio utente systemd insieme agli altri script per l'host:

php artisan vendor:publish --tag=griglia-scripts

Il worker è neutro rispetto al fornitore, tutto attorno al contratto della board: ogni istanza usa la propria chiave d'agente con griglia:check --agent=<chiave>, il proprio lock e gli stessi stati dei task. Ci sono driver di lancio integrati per Codex CLI e Claude Code; un template di argv in JSON collega un'altra CLI senza passare da una shell.

Installare il servizio utente systemd

Copia l'esempio e sostituisci /absolute/path/to/project in entrambe le righe con il percorso assoluto vero del progetto:

mkdir -p ~/.config/systemd/user
cp scripts/systemd/griglia-agent-worker@.service.example \
  ~/.config/systemd/user/griglia-agent-worker@.service
sed -i 's#/absolute/path/to/project#/srv/my-project#g' \
  ~/.config/systemd/user/griglia-agent-worker@.service
systemctl --user daemon-reload

Abilita un'istanza per ogni agente configurato. Il nome dell'istanza è la chiave dell'agente in Griglia:

systemctl --user enable --now griglia-agent-worker@codex.service
systemctl --user enable --now griglia-agent-worker@claude.service

Più applicazioni sullo stesso PC

Serve un worker per applicazione e per agente: ogni worker interroga una sola board e avvia l'agente nella directory del relativo progetto. Il lock include automaticamente repository e chiave agente, quindi due applicazioni possono usare entrambe codex senza bloccarsi tra loro.

Per ogni applicazione copia il template con un prefisso unità univoco e sostituisci il percorso. Per esempio:

cp app-one/scripts/systemd/griglia-agent-worker@.service.example \
  ~/.config/systemd/user/griglia-agent-worker-app-one@.service
sed -i 's#/absolute/path/to/project#/srv/app-one#g' \
  ~/.config/systemd/user/griglia-agent-worker-app-one@.service

cp app-two/scripts/systemd/griglia-agent-worker@.service.example \
  ~/.config/systemd/user/griglia-agent-worker-app-two@.service
sed -i 's#/absolute/path/to/project#/srv/app-two#g' \
  ~/.config/systemd/user/griglia-agent-worker-app-two@.service

systemctl --user daemon-reload
systemctl --user enable --now griglia-agent-worker-app-one@codex.service
systemctl --user enable --now griglia-agent-worker-app-two@codex.service

La configurazione comune dell'agente resta in ~/.config/griglia-worker/codex.env. Le impostazioni specifiche del progetto possono sovrascriverla in ~/.config/griglia-worker/griglia-agent-worker-app-one-codex.env (schema %p-%i.env della unità). In particolare ogni applicazione Docker deve indicare un GRIGLIA_WORKER_CONTAINER diverso; per il trasporto locale imposta invece GRIGLIA_WORKER_REPO e, se serve, GRIGLIA_WORKER_PHP.

codex invoca codex exec --approve-for-me; claude invoca claude -p --permission-mode bypassPermissions. L'unit aggiunge %h/.local/bin al PATH, il posto solito dei launcher installati dall'utente. Se command -v codex o command -v claude indicano un'altra cartella, metti una riga PATH=... completa in ~/.config/griglia-worker/<chiave-agente>.env.

Per guardare il servizio e seguirne l'output:

systemctl --user status griglia-agent-worker@codex.service
journalctl --user -u griglia-agent-worker@codex.service -f

Per tenere in piedi i servizi utente dopo il logout e farli partire all'avvio, abilita una volta il lingering:

loginctl enable-linger "$USER"
loginctl show-user "$USER" -p Linger   # atteso: Linger=yes

Configurazione

Ogni istanza legge, se c'è, ~/.config/griglia-worker/<chiave-agente>.env:

GRIGLIA_WORKER_DRIVER=codex
GRIGLIA_WORKER_INTERVAL=10
GRIGLIA_WORKER_RETRY_DELAY=30
GRIGLIA_WORKER_MAX_PARALLEL=2
GRIGLIA_WORKER_TRANSPORT=auto
GRIGLIA_WORKER_CONTAINER=laravel-dev-app
GRIGLIA_WORKER_REPO=/srv/my-project

Il trasporto di default è auto: all'avvio il worker verifica <container>, esegue docker exec <container> php artisan se risponde e altrimenti php artisan dentro il repository, stampando quale ha scelto. Fissalo su local dove Laravel gira direttamente sull'host del worker — così Docker non compare da nessuna parte nel giro:

GRIGLIA_WORKER_TRANSPORT=local
GRIGLIA_WORKER_PHP=/usr/bin/php8.4
GRIGLIA_WORKER_REPO=/srv/my-project

I nomi GRIGLIA_WORKER_* configurano una singola istanza. Quando mancano, il worker ripiega sulle variabili che leggono gli altri script sull'host — GRIGLIA_TRANSPORT, GRIGLIA_PHP, GRIGLIA_CONTAINER — così una scelta sola, esportata una volta per la macchina, copre sia il worker sia gli script che l'agente lancia da sé (conteggio dei token, sincronizzazione di contesto e skill). Ogni impostazione ha anche il suo flag, comodo per un lancio una tantum:

Flag Variabile d'ambiente Default
--transport auto\|docker\|local GRIGLIA_WORKER_TRANSPORT, GRIGLIA_TRANSPORT auto
--container GRIGLIA_WORKER_CONTAINER, GRIGLIA_CONTAINER laravel-dev-app
--php GRIGLIA_WORKER_PHP, GRIGLIA_PHP php
--repo GRIGLIA_WORKER_REPO cartella corrente
--driver codex\|claude\|custom GRIGLIA_WORKER_DRIVER la chiave dell'agente
--interval, --retry-delay GRIGLIA_WORKER_INTERVAL, GRIGLIA_WORKER_RETRY_DELAY 10, 30
--max-parallel GRIGLIA_WORKER_MAX_PARALLEL 2
--model GRIGLIA_WORKER_MODEL il default della CLI dell'agente
--effort GRIGLIA_WORKER_EFFORT il default della CLI dell'agente

Il driver di default è la chiave dell'agente, quindi le chiavi che si chiamano codex e claude non hanno bisogno di alcun file env. Se la chiave è diversa, indica il driver in modo esplicito.

Modello e livello di ragionamento

Senza altra configurazione ogni sessione usa il modello con cui è configurata la CLI dell'agente. GRIGLIA_WORKER_MODEL e GRIGLIA_WORKER_EFFORT li scelgono per singolo worker, così l'agente della board può girare su un modello diverso da quello delle sessioni interattive della stessa CLI:

GRIGLIA_WORKER_MODEL=fable
GRIGLIA_WORKER_EFFORT=max

Il driver claude li passa come --model e --effort (low, medium, high, xhigh, max); il driver codex come --model e -c model_reasoning_effort="<effort>". Il driver custom li riceve come segnaposto {model} e {effort} (stringa vuota se non impostati), quindi è il template argv a decidere dove finiscono. Il worker non valida i valori: un modello o un livello sconosciuto fallisce dentro la CLI dell'agente.

Sceglierli dalla board

I valori del worker sono il default: la board può cambiarli per lista e per task. Dichiara quali modelli ed effort offre ogni agente e compaiono due tendine — una nella barra della lista, una nella targhetta sotto il titolo del task (e fra i comandi del modale):

GRIGLIA_AGENT_MODELS="claude:opus=Opus,sonnet=Sonnet;codex:gpt-5,gpt-5-codex"
GRIGLIA_AGENT_EFFORTS="low,medium,high,xhigh,max"

Un gruppo per agente (chiave:valori), separati da ;; un elenco senza agente vale per tutti; valore=Etichetta rinomina una voce nell'interfaccia. Senza queste variabili non cambia niente: nessuna tendina, e ogni sessione usa il default del worker.

Ogni tendina ha la sua etichetta — Agente, Modello, Effort — nella barra della lista, sotto il titolo del task e fra i comandi del modale.

Dichiara alla board con quale valore parte già la CLI e la board lo nomina, invece di scrivere un generico Predefinito CLI:

GRIGLIA_AGENT_DEFAULT_MODEL="claude:opus;codex:gpt-5-codex"
GRIGLIA_AGENT_DEFAULT_EFFORT=high

Stessa forma del catalogo. Dove non si è scelto niente la board legge allora Predefinito (opus) nelle tendine e (opus) sulla targhetta: le parentesi dicono «qui non l'ha scelto nessuno, e girerà così». Le due variabili sono solo una didascalia: la board non le invia mai, quindi ogni worker tiene il suo GRIGLIA_WORKER_MODEL (mettici gli stessi valori perché la didascalia resti vera).

Un task usa il proprio valore, altrimenti quello della lista, altrimenti quello del worker. La targhetta mostra il valore effettivo, il modale lo ripete fra i comandi, griglia:check lo stampa accanto al titolo ({agent: claude, model: opus, effort: high}, oppure model: (opus) per un default dichiarato che nessuno ha cambiato) e il worker lo legge da --worker-json quando lancia la sessione. I valori che l'agente non offre vengono ignorati: riassegnare un task a un altro agente lascia cadere un modello che quell'agente non conosce, invece di far fallire la sua CLI. Mentre il task è in lavorazione le tendine sono bloccate: la sessione è già partita.

Le variabili si leggono all'avvio del worker. Per applicare una modifica senza interrompere le sessioni in corso, svuota il worker invece di riavviarlo — vedi Aggiornare un worker in esecuzione.

Per Gemini CLI, Aider o un altro agente, usa il driver custom. L'array JSON viene eseguito direttamente (mai attraverso una shell); {prompt}, {repo}, {agent}, {model} e {effort} vengono sostituiti dentro i singoli argomenti:

GRIGLIA_WORKER_DRIVER=custom
GRIGLIA_WORKER_COMMAND_JSON=["agent-cli","--cwd","{repo}","--prompt","{prompt}"]

Trasporto e driver sono indipendenti, quindi Codex, Claude e i driver personalizzati funzionano in tutte e due le modalità. L'utente del servizio deve poter usare Docker o l'eseguibile PHP locale configurato, e la CLI dell'agente scelto in modo non interattivo. Non usare flag che disattivano del tutto sandbox e approvazioni: concedi solo i permessi sul progetto che servono al flusso di lavoro.

Comportamento e prove

Il worker interroga lo stato corrente della board, quindi trova anche il lavoro già aperto prima di un riavvio. In modalità ordered esegue esattamente una sessione. In modalità multitasking esegue fino a --max-parallel sessioni (2 per default), una per task idoneo; riduci il limite quando i task possono modificare gli stessi file. Un flock per coppia repository/agente impedisce worker duplicati, mentre il worker tiene traccia di ogni processo figlio per task. Prima di avviare la CLI per un task aperto, il worker lo prende con griglia:check --take: così ripete il controllo di assegnazione corrente della board. Se l'utente ha cambiato l'agente del task o il predefinito della lista dopo lo snapshot, la board rifiuta la presa obsoleta e l'agente sbagliato non parte. Uno Stop termina solo il processo di quel task; quando un figlio esce, il suo slot torna disponibile e nel journal compare task <id>: agent session ended with status <codice>. Il worker legge solo il documento JSON di --worker-json: un avviso che la board stampa dopo non ferma il ciclo.

Aggiornare un worker in esecuzione

Un semplice systemctl --user restart uccide le sessioni dell'agente avviate dal worker. Per questo il worker si tiene aggiornato da solo, senza riavvio:

  • Nuovo script su disco — un rilascio del package, vendor:publish --tag=griglia-scripts, un git pull: entro un intervallo il worker si ri-esegue sul posto. Stesso PID, stesso lock, e ogni sessione in corso passa al nuovo codice (--adopt), quindi nessuno viene interrotto; nel journal compare reloading worker from <percorso> (<n> running session(s) carried over) e poi carried over after reload: …. Un file che non compila viene ignorato finché non cambia di nuovo.
  • Nuovo ambiente — ~/.config/griglia-worker/<chiave-agente>.env, per esempio GRIGLIA_WORKER_MAX_PARALLEL: lo legge il service manager all'avvio, quindi il worker deve ripartire, ma alle sue condizioni. Mandagli SIGHUP:

    systemctl --user kill --signal=SIGHUP --kill-whom=main griglia-agent-worker@codex.service
    

    Il worker non avvia nuove sessioni, lascia finire quelle in corso e poi esce; Restart=always nella unit lo fa ripartire con ambiente e script correnti. Nel journal compaiono SIGHUP received: draining … e drained: exiting so the service manager restarts the worker. Il lavoro aperto nel frattempo aspetta il riavvio — pochi secondi dopo la fine dell'ultima sessione.

Usa systemctl --user restart solo quando interrompere le sessioni in corso è accettabile.

Per controllare la configurazione senza lanciare un agente:

python3 scripts/griglia-agent-worker.py --agent=codex --driver=codex --once --dry-run
python3 scripts/griglia-agent-worker.py --agent=codex --transport=local --php=/usr/bin/php8.4 \
  --repo=/srv/my-project --once --dry-run

Il comando legge la board attraverso il trasporto scelto e stampa l'argv che eseguirebbe, così un errore qui è un problema di trasporto o di permessi, non dell'agente.

Per una prova da capo a fondo, abilita il servizio, crea un task innocuo assegnato a quell'agente e mettilo open to work. Nel journal dovresti vedere dispatching task <id> to <agent> e sulla board il task dovrebbe passare da aperto a working a fatto. Chiudere il terminale da cui hai fatto la prova non tocca il servizio systemd.

Per disabilitare un'istanza:

systemctl --user disable --now griglia-agent-worker@codex.service

Vedi anche

Griglia v0.95.0