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:
@@ -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).
|
||||
Reference in New Issue
Block a user