skill: subagent-supervision, protocollo completo dei sotto-agenti pi

Aggiorna e ottimizza la skill con tutto ciò che è emerso nell'uso reale: avvio
(brief che punta ai documenti esistenti invece di duplicarli, scelta del
thinking per tipo di lavoro), canale (registry come segnale di prontezza, SSE,
long-poll race-free con cursore seq), confini di attesa (turn_end è per turno,
settled è "in procinto", agent_settled è quello definitivo), gestione di
contesto e costo (blocco context in ogni risposta, soglia di compattazione,
report obbligatorio come leva di costo), iniezione idempotente e semantica del
409, ciclo di vita graceful con shutdown e prune dei soli morti, disciplina di
lettura delle trascrizioni, sette trappole già pagate e le regole di evidenza
da pretendere da un sub-agente (dispersione misurata, esiti negativi, gate di
correttezza prima della velocità).
This commit is contained in:
enne2
2026-09-26 20:10:00 +02:00
parent f9b1042a93
commit a3842e4b28
+204
View File
@@ -0,0 +1,204 @@
---
name: subagent-supervision
description: Avvia, supervisiona e controlla sotto-agenti pi (sub-agents) in finestre TUI separate, con un canale di controllo locale (HTTP su loopback + mailbox su file) per iniettare input e leggerne i risultati. Usare quando si parla di sotto-agenti, sub-agents, agenti paralleli, delegare un task a una sessione pi separata, leggere o impostare il contesto di un'altra sessione, o controllare una sessione pi in corso.
---
# Supervisione di sotto-agenti pi
Un sotto-agente e' una **sessione pi separata con la sua TUI**, in una finestra o tab
propria: l'utente la vede e la supervisiona direttamente, e l'agente supervisore la
controlla tramite il canale locale dell'estensione `subagent-control`.
Skill = protocollo (questa procedura). Estensione = canale (il codice che inietta).
Una skill da sola **non** puo' parlare con un altro processo: l'iniezione la fa
l'estensione.
## Canale di controllo
Ogni sessione che carica `subagent-control` registra se stessa:
```
~/.pi/agent/subagents/<id>.json {id, pid, port, token, cwd, started, hasUI,
ready, busy, turns, injected, lastEvent, lastEventAt} (0600)
~/.pi/agent/subagents/<id>.inbox mailbox: una riga = un messaggio iniettato
```
**Il registry e' il segnale di prontezza**: viene scritto a fine avvio sessione, quindi
**si polla il file, mai `sleep` a indovinare**. E viene riscritto a ogni cambio di stato,
quindi un semplice `cat` dice gia' se la sessione e' occupata (`busy`).
Endpoint (bind solo su loopback, token obbligatorio):
```bash
curl -s -H "X-Pi-Token: $TOKEN" http://127.0.0.1:$PORT/status
curl -s -H "X-Pi-Token: $TOKEN" -d '{"text":"nuovo contesto: ..."}' http://127.0.0.1:$PORT/say
curl -s -H "X-Pi-Token: $TOKEN" -d '{"text":"...","requestId":"step-7"}' http://127.0.0.1:$PORT/say
curl -s -m 130 -H "X-Pi-Token: $TOKEN" -d '{}' "http://127.0.0.1:$PORT/wait?timeout=120"
curl -N -H "X-Pi-Token: $TOKEN" "http://127.0.0.1:$PORT/events?since=0"
echo "input dalla mailbox" >> ~/.pi/agent/subagents/<id>.inbox
```
### Contesto, compattazione, ciclo di vita (v0.5)
```bash
# quanto e' pieno il contesto (tokens, contextWindow, percent, pending)
curl -s -H "X-Pi-Token: $TOKEN" http://127.0.0.1:$PORT/context
# compattazione su richiesta (NON bloccante: risposta 202 + evento compaction_done)
curl -s -H "X-Pi-Token: $TOKEN" -d '{}' http://127.0.0.1:$PORT/compact
curl -s -H "X-Pi-Token: $TOKEN" -d '{"customInstructions":"tieni solo le decisioni e i percorsi file"}' http://127.0.0.1:$PORT/compact
# fermare la direzione sbagliata / terminare in modo pulito
curl -s -X POST -H "X-Pi-Token: $TOKEN" http://127.0.0.1:$PORT/abort
curl -s -X POST -H "X-Pi-Token: $TOKEN" http://127.0.0.1:$PORT/shutdown
```
Il messaggio iniettato **appare nella TUI come messaggio utente** e fa partire un turno:
l'utente vede cosa sta facendo il sotto-agente mentre accade.
### Attesa: come si sa che ha finito
Non si polla la trascrizione e non si aspetta a tempo. Il canale emette eventi (SSE,
con `id:` per riconnettersi via `since`):
| evento | significato |
|---|---|
| `ready` | canale in ascolto |
| `injected` | input accettato (source, requestId, deliverAs) |
| `turn_start` | l'agente e' occupato |
| `turn_end` | un turno e' finito, ma puo' seguirne altro |
| **`settled`** | **l'agente non continuera' da solo: e' inattivo e pronto a nuovo input** |
**Regola operativa (a prova di race)**: leggere il `seq` da `/status`, **poi** iniettare,
**poi** attendere con `POST /wait?since=<seq>&timeout=N`: il long-poll ritorna sul
prossimo `settled` successivo a quel cursore. Senza `since` il cursore e' l'istante
della chiamata e un evento gia' passato puo' essere perso (si finirebbe in timeout).
Da bash e' **una sola chiamata** che ritorna quando il lavoro e' finito: meglio di un
`curl -N` in background da presidiare. `GET /events` serve solo per supervisione
continua (o per una UI), non per il ciclo "inietta e aspetta".
### Idempotenza
Un input iniettato due volte e' un doppio lavoro: passare sempre un `requestId` stabile
(es. `step-7`). Ripetere la stessa richiesta con lo stesso `requestId` e' sicuro: il
secondo invio torna `{"ok":true,"duplicate":true}` senza iniettare.
### Modalita' di consegna
`deliverAs` (valori verificati nell'API): assente = accoda e fa partire un turno se
inattivo; `steer` = corregge il turno in corso; `followUp` = consegna dopo il turno
corrente; `nextTurn` = tiene per il turno successivo. Usare `steer` per fermare una
direzione sbagliata senza uccidere la sessione.
## Avviare un sotto-agente visibile
```bash
cd <progetto> && setsid konsole -e bash -lc \
'cd <progetto> && exec pi --name "<nome breve>" "@<file-di-contesto>" "<primo task>"'
```
- Contesto iniziale: file di handover/prompt passati con `@file` (o `--append-system-prompt <file>`).
- Aggiungere `--session-id <id-stabile>` se la sessione va ripresa in seguito.
- **Non** usare `--mode rpc` se serve la TUI: il mode rpc *sostituisce* l'interfaccia
terminale con un flusso JSONL su stdin/stdout.
## Elencare e ripulire
Il canale elenca da solo tutte le sessioni aperte, con porta e verdetto di
sopravvivenza, tramite heartbeat (non serve piu' globare i file):
```bash
curl -s -H "X-Pi-Token: $TOKEN" http://127.0.0.1:$PORT/sessions
curl -s -H "X-Pi-Token: $TOKEN" "http://127.0.0.1:$PORT/sessions?prune=1" # rimuove solo i morti
```
Ogni record include `alive`, `pidAlive`, `heartbeatFresh` e `heartbeatAgeMs`.
Un heartbeat piu' vecchio di 30 s significa sessione morta **anche se il processo
esiste**: e' il caso che `kill -0` da solo non sa distinguere (processo vivo ma
bloccato). La freschezza protegge una sessione appena avviata: non viene mai
potata d'ufficio.
Per terminare un sotto-agente usare **`POST /shutdown`** (chiusura *graceful*,
che rimuove anche il record dal registry): `kill <pid>` resta solo come ultima
risorsa se il canale non risponde.
```bash
curl -s -X POST -H "X-Pi-Token: $TOKEN" http://127.0.0.1:$PORT/shutdown
```
## Leggere i risultati (disciplina obbligatoria)
La trascrizione e' un JSONL in `~/.pi/agent/sessions/--<cwd-con-/-->--/<timestamp>_<id>.jsonl`.
**Non leggere il file intero**: cresce in fretta (una sessione di lavoro reale ha
superato i 500 KB). Leggere la coda e filtrare:
```bash
F=<trascrizione>
python3 - "$F" <<'PY'
import json,sys
msgs=[]
for line in open(sys.argv[1]):
try: d=json.loads(line)
except Exception: continue
m=d.get("message") or {}
if d.get("type")=="message" and m.get("role")=="assistant":
c=m.get("content")
t=" ".join(x.get("text","") for x in c if isinstance(x,dict)) if isinstance(c,list) else str(c)
if t.strip(): msgs.append(t)
print("\n---\n".join(msgs[-2:])[:3000])
PY
```
Regole: solo i messaggi `assistant`, solo gli ultimi 1-3, troncati; i `toolResult`
si ignorano (sono rumore per il supervisore). Per lo stato operativo usare `/status`,
non il transcript.
## Best practice di interazione (v0.5)
1. **Prima di assegnare un nuovo task, leggi `/context`.** Se `percent` e' alto,
compatta **prima** di iniettare il prompt successivo: una sessione lunga che
va in overflow a meta' lavoro perde il filo e ti fa perdere il lavoro.
2. **Ciclo di compattazione**: `POST /compact` (risposta 202, non bloccante) →
attendi l'evento `compaction_done` (o `compaction_failed`) → poi `POST /wait`
sul `settled` → **solo allora** inietti il prompt.
`tokens` puo' risultare `null` subito dopo: significa "non ancora noto", non errore.
3. **`fromExtension` distingue le compattazioni**: se e' `false` la compattazione
e' arrivata da soglia automatica o da `/compact` dell'utente — non attribuirtela.
4. **`steer` vs `abort`**: `steer` corregge la direzione lasciando lavorare;
`abort` ferma l'operazione in corso. Usa `abort` quando il lavoro sta andando
nella direzione sbagliata e vuoi ripartire da un contesto pulito.
5. **Controlla `pending` prima di iniettare**: se ci sono messaggi gia' in coda,
un altro input si accoda dietro invece di essere eseguito subito.
6. **Termina con `/shutdown`**, non con `kill`: chiude in modo pulito e rimuove il
record dal registry, quindi le liste restano veritiere.
7. **Il contesto arriva dentro ogni risposta** (blocco `context`, con anche
`lastResponse`: token di input/output dell'ultima risposta): non serve chiamare
`/context` a parte — leggilo dal risultato di `/say` o di `/wait`.
8. **`/say` rifiuta con 409 se c'e' gia' input in coda** (`pending`): per accodare
di proposito servono `deliverAs` (steer/followUp/nextTurn) o `force:true`.
Un 409 non e' un errore da ritentare: significa che devi prima attendere.
7. **Una sola fonte di verita' per lo stato**: `/status` (stato + contesto) — non
dedurre lo stato dalla trascrizione.
## Errori tipici
- **Modifica all'estensione + sessione gia' avviata = stai testando il codice vecchio.**
La sessione carica l'estensione all'avvio: dopo un edit va **terminata e riavviata**,
altrimenti la misura non riguarda la patch (stessa classe della trappola `.glsl` degli
shader Vulkan). Sintomo tipico che lo smaschera: due casi simili si comportano
diversamente, cosa possibile solo con il codice precedente.
- **`pkill -f "<pattern>"` uccide la propria shell** se il pattern compare nella
command line del comando stesso. Usare i PID (`kill <pid>`) letti dal registry.
- Sessione occupata: `/say` inietta comunque (accodato); la mailbox e' la scelta
sicura per input lunghi o multipli.
- Se `/status` non risponde: la sessione non ha l'estensione caricata, oppure e'
stata chiusa (voce stale nel registry).
## Ciclo di vita consigliato
1. Preparare il file di contesto del sotto-agente (obiettivo, vincoli, comandi, criterio di verifica).
2. Avviarlo in una tab konsole con `@contesto` e il primo task.
3. Supervisionare: `/status` per lo stato, mailbox o `/say` per correggere la rotta.
4. Alla fine: leggere gli ultimi messaggi `assistant`, riportare all'utente, ripulire le voci stale.
5. Se il lavoro produce modifiche al codice: **committare e pushare** prima di chiudere
(un albero non versionato e' a rischio).