skill: subagent-supervision, ottimizzazione del consumo di contesto

Aggiunge la sezione 4b: dove vanno i token (system prompt, output strumenti,
messaggi del supervisore, storia riletta a ogni turno), le sei leve ordinate per
efficacia reale (sub-agente come firewall di contesto, stabilità del prefisso per
non invalidare la cache del prompt, report invece di trascrizione, disciplina
sugli output degli strumenti, mai rileggere, concisione del supervisore) e le tre
regole di compattazione: non compattare presto né durante il lavoro, usare
customInstructions per conservare decisioni e numeri scartando il ragionamento
intermedio, scrivere il report prima di compattare così la summarization può
essere aggressiva senza perdere valore. Include la regola di misurare la
trascrizione per categoria prima di tagliare.
This commit is contained in:
enne2
2026-09-26 20:23:02 +02:00
parent a3842e4b28
commit d793d35759
+160 -164
View File
@@ -1,204 +1,200 @@
---
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.
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, seguirne gli eventi, gestirne contesto e costo 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, controllare una sessione pi in corso, o chiuderla.
---
# 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. Estensione = canale.** Una skill da sola non può parlare con un
altro processo: l'iniezione la fa l'estensione `subagent-control` (repo privato
`git.enne2.net/enne2/subagent-control`). Un sotto-agente è una **sessione pi con la
sua TUI**: l'utente la vede e la supervisiona, il supervisore la controlla via canale.
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:
## 1. Avviare un sotto-agente
```
~/.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
1. BRIEF → file `TASK-<nome>.md`: obiettivo, fatti già verificati (non da ridimostrare),
protocollo di misura, vincoli duri, cosa NON fare, deliverable continui.
PUNTA ai documenti esistenti (CHANGES.md, report) invece di duplicarli:
duplicare contesto si paga due volte.
2. LAUNCH → setsid konsole -e bash -lc "cd <dir> && exec pi --name <nome> \
--provider <p> --model <m> --thinking <livello> @TASK-<nome>.md '<primo prompt>'"
3. VERIFICA→ attesa ATTIVA del registry (polling, mai sleep), poi /status, poi etichetta,
poi il modello EFFETTIVO dalla trascrizione (vedi §8).
```
**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`).
**Scelta del modello**: `--thinking high` per ottimizzazione e diagnosi; `medium` per
misurazione, ricerca e manutenzione meccanica. Costo e ragionamento vanno allineati al
tipo di lavoro, non al prestigio. `GET /models` dice quali livelli ogni modello
supporta davvero (`thinkingLevelMap`, null = non supportato) e `costFullContext`.
Endpoint (bind solo su loopback, token obbligatorio):
## 2. Canale di controllo
```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
```
~/.pi/agent/subagents/<id>.json {id,pid,port,token,cwd,started,hasUI,ready,busy,turns,
injected,label,heartbeatAt,lastEvent,lastEventAt} (0600)
~/.pi/agent/subagents/<id>.inbox mailbox: una riga = un messaggio
```
### Contesto, compattazione, ciclo di vita (v0.5)
Endpoint (127.0.0.1, header `X-Pi-Token`; 403 token errato, 400 body, 404 route, 409 rifiutato):
`GET /status`, `/context`, `/sessions[?prune=1]`, `/models`, `/events` (SSE, `?since`,
ring buffer 200, heartbeat `: ping` 15 s); `POST /say`, `/wait`, `/model`, `/label`,
`/compact`, `/abort`, `/shutdown`.
```bash
# quanto e' pieno il contesto (tokens, contextWindow, percent, pending)
curl -s -H "X-Pi-Token: $TOKEN" http://127.0.0.1:$PORT/context
**Il registry è il segnale di prontezza** (scritto a fine avvio, riscritto a ogni cambio
di stato e ogni heartbeat di 10 s): si **polla il file**, mai `sleep` a indovinare. Un
`cat` dice già se la sessione è occupata.
# 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
## 3. Attesa: quali confini contano
# 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
```
| evento | significato | attendibile? |
|---|---|---|
| `turn_end` | **turno intermedio** (7 in un solo run) | ❌ |
| `agent_end` | run finito, ma possono seguire retry/continuazioni | ⚠️ |
| `settled` | *sta per* assestarsi (evento azionabile) | ⚠️ quasi |
| `agent_settled` | assestato **definitivamente** | ✅ (non ancora emesso: gap noto) |
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.
**Protocollo race-free**: leggere `seq` da `/status` → iniettare → `POST /wait?since=<seq>`
(long-poll che ritorna sul `settled`). Da bash è **una chiamata** che ritorna quando il
lavoro è finito: meglio di un `curl -N` in background da presidiare. `GET /events` serve
per la supervisione continua, non per il ciclo "inietta e aspetta".
Prima di agire, verifica **tre cose insieme**: `busy == false` e `lastEvent` di settle e
heartbeat fresco.
### Attesa: come si sa che ha finito
## 4. Contesto e costo (la parte che si dimentica)
Non si polla la trascrizione e non si aspetta a tempo. Il canale emette eventi (SSE,
con `id:` per riconnettersi via `since`):
- **Ogni risposta JSON porta il blocco `context`** (`tokens`, `contextWindow`, `percent`,
`pending`, `lastResponse`): l'orchestratore conosce lo stato **senza chiamare `/context`**.
Monitorare non deve costare una chiamata.
- `tokens: null` subito dopo una compattazione significa "non ancora noto", **non errore**.
- **Soglia ~60%**: `POST /compact` (202, non bloccante) → attendere `compaction_done` →
`POST /wait` sul `settled` → **solo allora** iniettare. Compattare molto prima è un
costo, non un risparmio; compattare durante il lavoro lo interrompe.
- `fromExtension: true` distingue le compattazioni che hai innescato tu.
- **`cacheRead`** dice quanto costa davvero: se è alto, il prompt caching sta pagando.
- **REPORT OBBLIGATORIO**: imporre al sotto-agente un file `REPORT-<nome>.md` aggiornato
**a ogni passo** (stato, numeri, prossimo passo, contesto). È la leva di costo più
efficace: leggere un report costa cento volte meno che ricostruire lo stato dalla
trascrizione, e permette di sapere cosa ha concluso **senza interromperlo**.
- **qmem**: far registrare i risultati (inclusi quelli negativi) e far correggere i
record sbagliati.
| 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** |
## 4b. Ottimizzare il consumo (dove vanno i token, e come tagliarli)
**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".
**Quattro secchi**: (1) system prompt + AGENTS.md + skill caricate; (2) **output degli
strumenti** (spesso il più grande, e resta in contesto per sempre); (3) i tuoi messaggi
(generati *e* riletti a ogni turno); (4) la storia riletta ogni turno = tutto quanto sopra
moltiplicato per i turni.
### Idempotenza
**Leve, in ordine di efficacia reale:**
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.
1. **Il sotto-agente è un firewall di contesto.** Assorbe letture file, misure e
fallimenti; al supervisore tornano poche centinaia di token. Nel caso reale: tre
agenti hanno bruciato 121k+72k+109k token, il supervisore ne ha letti ~2k. Fai girare
il lavoro pesante su modelli **più economici**: l'agenzia si prende il volume, tu la
decisione.
2. **Stabilità del prefisso = cache.** Misurato: `cacheRead` 47k su 47k e 110k su 121k →
**98-99% dell'input servito da cache**. Ogni modifica a `AGENTS.md`, alle skill o alle
estensioni **invalida il prefisso per tutti i turni successivi**: è il costo nascosto
più caro. Non toccare quei file a metà sessione.
3. **Report invece di trascrizione.** Leggere un report costa centinaia di token,
ricostruire lo stato dalla trascrizione migliaia. **Documenti che puntano, non che
duplicano**: un brief che dice "leggi CHANGES.md" costa 200 token, uno che lo ripete 5k.
4. **Disciplina sugli strumenti**: `head`/`tail`/`grep -c` invece di dump; `-o md` per
tabelle compatte; mai `cat` di file grandi. Ogni byte entra e non esce più.
5. **Mai rileggere** lo stesso file, né ridedurre fatti già in `CHANGES.md` o in qmem.
6. **Concisione del supervisore**: i messaggi lunghi restano in contesto e si rileggono a
ogni turno. Tabelle e numeri, non prosa.
### Modalita' di consegna
**Compattazione — tre regole:**
`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.
- **Non compattare presto, mai durante il lavoro.** Al 20% è un costo (paghi una
summarization per buttare via dettaglio che ti serve). Soglia ~60%, e solo dopo il
confine definitivo.
- **`customInstructions` per compattare bene** (conserva l'utilità, taglia di più):
`POST /compact {"customInstructions":"conserva decisioni, percorsi file, numeri misurati e comandi esatti; scarta il ragionamento intermedio e le ipotesi superate"}`
- **Prima il report, poi la compattazione.** Il sotto-agente scrive la sintesi in
`REPORT-*.md` **prima** di compattare: allora la summarization può essere aggressiva
senza perdere nulla, perché il valore è già fuori dal contesto. `tokens: null` subito
dopo significa "non ancora noto", non errore.
## Avviare un sotto-agente visibile
**Prima di tagliare, misura**: se non sai quale dei quattro secchi pesa, stai ottimizzando
a caso. Aggrega la trascrizione per categoria (byte per system prompt, output strumenti,
messaggi) e taglia quello che pesa davvero.
```bash
cd <progetto> && setsid konsole -e bash -lc \
'cd <progetto> && exec pi --name "<nome breve>" "@<file-di-contesto>" "<primo task>"'
```
## 5. Iniezione di input
- 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.
- **Sempre un `requestId` stabile**: un ritento torna `duplicate:true` invece di
iniettare due volte.
- **`409` = "aspetta", non "riprova"**: arriva quando la sessione è occupata (`busy`) o
ha messaggi in coda (`pending`). Per accodare di proposito: `deliverAs`
(`steer` corregge il turno in corso, `followUp` consegna dopo, `nextTurn` tiene per
dopo) oppure `force:true`.
- Un'iniezione **appare nella TUI come messaggio utente**: l'utente vede cosa accade.
Questo è il motivo per cui si inietta *messaggi* e non si riscrive il system prompt.
- Mailbox (`>> <id>.inbox`) per sessioni occupate o input lunghi.
## Elencare e ripulire
## 6. Ciclo di vita
Il canale elenca da solo tutte le sessioni aperte, con porta e verdetto di
sopravvivenza, tramite heartbeat (non serve piu' globare i file):
- **Chiudere con `POST /shutdown`** (graceful: termina e rimuove il record). `kill <pid>`
solo se il canale non risponde.
- **`GET /sessions?prune=1` rimuove SOLO i morti** (heartbeat stantio > 30 s). Un
heartbeat vecchio significa morta **anche se il processo esiste** (hang): `kill -0` da
solo non lo distingue.
- **Mai toccare sessioni che non sono tue.** Un record senza etichetta può essere di
un'altra persona o di un altro lavoro in corso: si chiede, non si pota e non si uccide.
Se una sessione è `busy` e non sai di chi sia, non toccarla **e dichiaralo**.
```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
```
## 7. Disciplina di lettura
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.
La trascrizione JSONL cresce (sessioni reali: > 100k token). **Mai leggerla intera**:
coda + filtro sui soli messaggi `assistant`, 1-3 messaggi, troncati. I `toolResult` si
ignorano. Per lo stato si usa `/status`, non la trascrizione. Il **report** è la fonte
che il sotto-agente deve tenere aggiornata: leggi quello.
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.
## 8. Trappole (tutte pagate almeno una volta)
```bash
curl -s -X POST -H "X-Pi-Token: $TOKEN" http://127.0.0.1:$PORT/shutdown
```
1. **Dopo aver modificato l'estensione, una sessione già avviata esegue il CODICE
VECCHIO** → riavviarla prima di testare. Sintomo diagnostico: due casi simili si
comportano diversamente (possibile solo col codice precedente). Stessa classe della
trappola `.glsl`/shader e delle build dir copiate.
2. **`pkill -f "<pattern>"` uccide la propria shell** se il pattern compare nella command
line stessa → terminare per **PID** letti dal registry.
3. **`--mode rpc` sostituisce la TUI**: per TUI + controllo serve l'estensione, non rpc.
4. **`@file` è rifiutato in rpc mode**: il contesto va iniettato con `--append-system-prompt`.
5. **Il modello effettivo si verifica dalla trascrizione** (`model_change`), non da
`/status` (che può non avere il modello) né dal comando di lancio: un agente può
cambiare modello da sé o essere cambiato dalla TUI. I file di sessione hanno **nome in
UTC**, non in ora locale.
6. **`-r N` non è "N invocazioni"**: la dispersione si misura fra invocazioni separate.
7. **I flag si verificano con `--help`**: `-c` non esiste in `llama-bench`, `-ckv` non
esiste affatto. Due volte pagato.
## Leggere i risultati (disciplina obbligatoria)
## 9. Economia della delega — cosa delegare e cosa pretendere
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:
- **Delega bene**: misurazioni ripetitive, esplorazioni con criterio, ottimizzazioni con
un protocollo chiaro, ricerche su fonti esterne, manutenzione. **Delega male**: decisioni
architetturali (le prendi tu), giudizi di priorità, e qualunque cosa richieda il
contesto di tutta la sessione.
- **Pretendi sempre**: (a) report aggiornato; (b) **dispersione, non numeri singoli**
(media, deviazione, span su 3+ invocazioni); (c) **esiti negativi registrati**; (d)
gate di correttezza **prima** della velocità; (e) una modifica alla volta; (f) nessun
download e nessuna modifica di configurazione senza approvazione; (g) mai push su
GitHub, solo su gitea.
- **Giudica contro lo span misurato, non contro una soglia fissa**: la stessa baseline
dava span 2,4% su una macchina e 0,2% su un'altra; e un candidato con span 5,98% ha
ucciso un "+3,3%" che sembrava reale. Non scegliere lo span storico più stretto per
rivendicare un guadagno: usa quello misurato adesso.
- **Il valore non sta dove sembra**: su questa macchina la speculazione ha dato +42-148%,
mentre il tetto della micro-ottimizzazione dei kernel era ~1,26×. Prima di delegare ore
di tuning, chiedersi se esiste una leva strutturale (speculazione, architettura, MoE).
```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
```
## 10. Errori tipici
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).
- Attendere un tempo fisso invece di pollare il registry, o usare `turn_end` come "finito".
- Compattare a contesto basso (costo) o durante il lavoro (interruzione).
- Iniettare senza `requestId`, poi ritentare e iniettare due volte.
- Interpretare un `409` come errore da ritentare.
- Terminare con `kill` ciò che si poteva chiudere con `/shutdown`.
- Toccare sessioni non proprie, o fidarsi di un record senza heartbeat.
- Ricostruire lo stato leggendo la trascrizione invece del report che avevi imposto.