Pi 1.0: l'harness minimale e il trucco codemode che risolve il gonfiore di token di MCP
Pi 1.0 è uscito il 1 ottobre 2026 e la mattina dopo era in cima a Hacker News con oltre 1.500 punti. La presentazione è quasi aggressivamente noiosa: "un harness per agenti minimale ed estensibile che puoi rendere tuo". Licenza MIT, TypeScript, un solo comando da terminale.
La presentazione lo svaluta. Sotto la TUI, Pi spedisce la risposta più completa che qualcuno abbia pubblicato al problema che ogni harness per agenti incontra al terzo mese: i server MCP distruggono la tua finestra di contesto, e nessuno vuole ammetterlo. Pi ha passato un anno a rifiutare pubblicamente il supporto a MCP, poi lo ha spedito — non mettendo affatto i tool nel prompt. Il meccanismo è documentato, misurabile e copiabile, che tu usi Pi o no.
Questa pagina parla di quel meccanismo, dei numeri che lo sostengono e delle tre decisioni di design di Pi che cambieranno il modo in cui pensi al tuo harness.
- Capire codemode: una sandbox QuickJS/WASM in cui le chiamate ai tool annidate non entrano mai nel contesto del modello — solo l'output dello script lo fa
- Imparare le quattro modalità di esposizione MCP di Pi e perché quella di default (codemode) non dichiara nulla al modello
- Vedere i budget esatti: 3.000 token per le dichiarazioni dei tool inlinate, 20 KB prima che un risultato MCP venga sventrato, 1 MiB di output bash che uno script può vedere e il modello no
- Copiare il trucco della cache del prompt: accodare al transcript la sezione di system prompt cambiata invece di mutare le definizioni dei tool
- Conoscere le omissioni deliberate (nessun sub-agent, nessuna plan mode, nessuna approvazione per singola chiamata) e il buco di fiducia che carica l'AGENTS.md di un repo clonato senza chiedere
La versione in un paragrafo
Pi è un agente da terminale agnostico rispetto al modello, sviluppato da
Earendil (che costruisce anche Radius e Lefos). Il repo —
github.com/earendil-works/pi — è un monorepo TypeScript di tredici pacchetti
utilizzabili indipendentemente: un'API LLM unificata su ~30 provider, un agent
loop, una libreria TUI, un client MCP standalone, una sandbox WASM e un runtime
durevole sperimentale. Richiede Node.js 22.19+. Spedisce deliberatamente
senza sub-agent e senza plan mode, e non chiede approvazione prima di
ogni chiamata a un tool. Quello che spedisce al loro posto è context engineering:
codemode, una sandbox JavaScript per cui il modello scrive script, dove le
chiamate ai tool avvengono fuori dal transcript e torna solo l'output
distillato dello script.
Perché conta anche se non lo installerai mai
Ogni harness inizia nello stesso modo. Aggiungi tre server MCP perché sembrano utili. Ognuno riversa venti definizioni di tool nel system prompt. Ora ogni singola chiamata al modello in ogni sessione paga sessanta schemi di tool, la maggior parte dei quali non verrà mai chiamata, e la tua cache del prompt si rompe ogni volta che un server si riconnette e la lista dei tool cambia.
AILmanac lo ha già misurato — vedi Quanto ti costano davvero i server MCP in token. La risposta di Pi è strutturalmente diversa da "usa meno server".
Codemode: il meccanismo vero
Il tool codemode prende come input sorgente JavaScript grezzo — non JSON,
non un blocco di codice delimitato. Pi lo esegue come corpo di una funzione async
dentro una VM QuickJS compilata in WebAssembly, così await e return al
livello più esterno funzionano.
L'unica capacità della sandbox è chiamare i tool iniettati. Non ci sono timer,
né fetch, né process, né require, né moduli, né WebAssembly annidata.
(eval e Function funzionano, ma producono solo altro codice dentro la stessa
VM.) Gli URL remoti delle immagini vengono rifiutati; passano solo URL base64
data: e blocchi immagine.
La conseguenza è tutto il punto:
Le chiamate ai tool annidate non entrano mai nel contesto dell'LLM. Solo l'output e il valore di ritorno dello script lo fanno.
Così uno script può diramare dieci chiamate in parallelo, filtrare 400 KB di risultati fino a quattro righe, e il modello vede quattro righe. Le dieci chiamate, i loro argomenti e i loro risultati grezzi non sono mai voci del transcript.
- Dentro uno script, ogni tool chiamabile è un metodo su `tools`. I caratteri non validi in un identificatore JavaScript diventano underscore, così il tool MCP `mcp__dev-radius__search` è raggiungibile come `tools.mcp__dev_radius__search`. Il modello non ha bisogno che siano dichiarati nel suo prompt per chiamarli.
- Gli script ottengono `ALL_TOOLS` (ogni tool chiamabile come nome più descrizione), `searchTools(query)` che classifica i tool per rilevanza BM25 con un limite di default di 8, `describeTool(name)` che restituisce la dichiarazione TypeScript di un tool e `describeNamespace(name)` che restituisce descrizione, istruzioni e nomi dei tool di un server MCP. La scoperta dei tool diventa una query a runtime, non uno scarico al momento del prompt.
- Uno script può iniziare con una riga di opzioni. `max_output_tokens` vale 10.000 per default; un output più lungo conserva l'inizio e la fine, e il testo completo viene scritto in un file temporaneo il cui percorso è incluso nel risultato — così il modello può andare a leggerlo con un tool se davvero gli serve. `timeout_ms` non è impostato per default ed è una scadenza rigida per l'intero script.
- La documentazione è esplicita: le chiamate fatte prima di un fallimento non vengono annullate. Le chiamate ancora in corso quando lo script finisce vengono cancellate, e le promise non attese vengono scartate. Uno script fallito conserva il suo output parziale, poi riporta `Script error:` e l'errore. Scrivi script che siano sicuri da rieseguire, perché un timeout alla settima chiamata su dieci ne lascia sei fatte.
- `store(key, value)` e `load(key)` conservano JSON tra le chiamate codemode. Le scritture vengono mantenute solo se lo script riesce, e ogni script riuscito che memorizza valori accoda alla sessione una voce `codemode-store` — così una sessione ripresa conserva i suoi valori, e un branch forkato vede solo ciò che è stato scritto sul proprio percorso.
Abilita codemode senza alcun server MCP, poi provalo
# Pi activates codemode automatically when an MCP server with
# "codemode" exposure connects. To have it always on, add it to
# ~/.pi/agent/settings.json alongside the default tools:
#
# { "defaultTools": ["+codemode"] }
pi
# Then ask for something that would normally flood the context, e.g.:
# "Use codemode: run the test suite, and return only the names of
# failing tests plus the first assertion line of each."
#
# The full test output never becomes a transcript entry.Il numero di bash che vale la pena ricordare
Una chiamata diretta a bash mostra al modello un risultato limitato a 2.000
righe o 50 KB. Chiamato dall'interno di uno script codemode, lo stesso tool
risolve in un valore strutturato il cui campo output contiene fino a 1 MiB
— e quando è più lungo, conserva i primi e gli ultimi 512 KiB attorno a un
marcatore di omissione, imposta truncated e indica l'output completo in
full_output_path.
È una finestra 20× più grande sull'output dei comandi per lo script, che poi
decide cosa il modello può vedere. Lo stesso per MCP: i risultati testuali
oltre i 20 KB raggiungono il modello con il centro rimosso attorno a un
marcatore …N chars truncated…, mentre uno script codemode riceve il
CallToolResult completo, incluso structuredContent e isError.
Le quattro modalità di esposizione
Questa è la tabella da copiare nelle tue note di design. Ogni server MCP in Pi
ha un'exposure, che decide come il modello può raggiungere i tool di quel
server. Ogni tool è registrato come mcp__<server>__<tool>.
| Exposure | Cosa vede il modello | A cosa serve |
|---|---|---|
codemode (default) | Niente. Non dichiarato, e nemmeno elencato nella descrizione di codemode. Gli script lo trovano con searchTools(), describeTool() o ALL_TOOLS. | Server generici, specialmente quando gli script dovrebbero combinare o filtrare le chiamate |
deferred | Niente, finché tool_search non carica una corrispondenza per la chiamata successiva al modello. Poi è dichiarato e resta dichiarato su quel branch. | Server grandi i cui tool vanno chiamati direttamente dopo la scoperta |
direct | Dichiarato come un tool integrato, e chiamabile anche da codemode. | Insiemi di tool piccoli e usati di frequente |
hidden | Registrato ma non raggiungibile. | Server o tool che devono restare non disponibili |
toolExposure sovrascrive l'impostazione a livello di server per singolo tool.
Le chiavi sono nomi esatti di tool o pattern *; i nomi esatti battono i
pattern, e tra i pattern vince la prima corrispondenza. Così un server hidden
può esporre esattamente un tool, o un server deferred può tenere i suoi tool
distruttivi fuori portata:
Esposizione per singolo tool in .pi/mcp.json
{
"mcpServers": {
"github": {
"exposure": "deferred",
"toolExposure": {
"search_code": "direct",
"get_*": "codemode",
"delete_*": "hidden"
}
}
}
}Due dettagli facili da perdere. Primo, i tool codemode e deferred sono
raggiungibili attraverso entrambi i meccanismi indiretti — gli script possono
chiamarli e tool_search può caricarli — e le chiamate codemode non dipendono
dall'insieme di tool attivo, quindi funzionano ancora dopo un cambio di branch,
una ripresa o un fork. Secondo, Pi avvisa una volta se né codemode né
tool_search sono attivi, perché allora i tuoi tool non diretti sono
semplicemente non chiamabili.
Il trucco della cache del prompt
Ecco l'idea più copiabile di questa pagina.
Pi connette in background ogni server MCP abilitato all'avvio di una sessione. I
server con tool codemode o deferred vengono elencati in una sezione
mcp_servers del system prompt — una riga ciascuno, dalla description
configurata o, una volta connessi, dalle istruzioni del server stesso.
Quella sezione cambia man mano che i server si connettono. Un harness ingenuo riscriverebbe il system prompt o le dichiarazioni dei tool, invalidando ogni prefisso in cache dietro di esse. Pi invece:
accoda la nuova sezione alla conversazione invece di cambiare le dichiarazioni dei tool, così i messaggi precedenti restano in cache.
Un prefisso cambiato ti costa l'intera cache. Un suffisso cambiato non ti
costa niente. Pi trasforma una mutazione del prompt in un'aggiunta al transcript,
ed è per questo che anche la descrizione del tool codemode è progettata per non
cambiare mentre i server si connettono — i tool con esposizione deferred ne
vengono deliberatamente lasciati fuori.
Il primo prompt attende fino a 10 secondi solo per i server con tool
direct, perché quelli devono essere dichiarati nella richiesta. Tutto il resto
viene atteso in modo lazy: uno script attende i server che nomina, e li attende
tutti solo se chiama searchTools() o legge ALL_TOOLS.
Se stai costruendo sull'API di Anthropic, lo stesso ragionamento vale per i tuoi prefissi — vedi Prompt caching e Cambi di tool a metà conversazione, che è esattamente la capacità che Pi 1.0 chiama "mid-conversation system messages".
I budget, in una tabella
| Cosa | Valore | Dove |
|---|---|---|
Dichiarazioni dei tool inlinate nella descrizione di codemode | 3.000 token stimati (caratteri ÷ 4); 0 elenca solo i namespace | codemode.inlineBudget |
max_output_tokens di default per l'output di uno script | 10.000 | riga di opzioni di codemode |
Numero di risultati di default di searchTools() | 8 (ranking BM25) | globali di codemode |
| Risultato testuale MCP prima che il centro venga rimosso | 20 KB | risultati dei tool MCP |
Output di bash che il modello vede direttamente | 2.000 righe o 50 KB | tool integrato |
Output di bash che uno script vede | fino a 1 MiB (primi + ultimi 512 KiB se più lungo) | codemode |
| Un valore nello store / tutti i valori | 262.144 / 1.048.576 caratteri di JSON | store di codemode |
| Chiamate concorrenti a classificatori o modelli immagine per script | 4 | models di codemode |
| Retry HTTP MCP su 408/429/5xx | 2 | trasporto MCP |
| Limite del nome dei tool accettato dai provider | 64 caratteri di [A-Za-z0-9_-] | pi-mcp |
Con codemode.mode impostato su "only" invece del default "on", i tool
integrati e quelli delle estensioni vengono nascosti del tutto al modello ed
elencati invece nella descrizione di codemode — il modello raggiunge ogni cosa
attraverso gli script. È la versione più estrema dell'idea, ed è a una chiave di
impostazione di distanza.
Modelli non-LLM dentro la sandbox
Il globale models in uno script codemode può eseguire modelli classificatori
e immagine con le credenziali della sessione. I modelli di chat sono elencati
ma non possono essere eseguiti da uno script — quel confine è deliberato.
L'interfaccia del classificatore prende un oggetto state più una mappa di
domande, e risponde a tutte in una sola chiamata. Le domande hanno tre forme:
choice (scegli un'etichetta, con criteria che mappa ogni etichetta al suo
significato), score (una scala ordinata descritta dal valore più basso) e
bool. Le risposte tornano con probabilità e una confidenza, non con prosa.
È la stessa categoria System One / modelli decisionali che AILmanac copre in
Modelli decisionali System One: Jev, Kev e i cloni open
e Addestra il tuo modello decisionale.
Pi spedisce TypeSafe come provider di prima classe, e la documentazione nota
che gli ID dei modelli differiscono per provider — typesafe/jev-latest contro
openrouter/typesafe/jev-1.13 — quindi li risolvi con
models.getAvailableOfType("classifier") invece di scriverli a mano.
Il pattern che questo sblocca: il tuo modello di frontiera scrive uno script che estrae 150 issue da un server MCP, classifica ognuna con un classificatore calibrato ed economico a concorrenza quattro, e restituisce le sei che contano. Le 150 issue non toccano mai il contesto del modello costoso.
- `classify()` e `generateImages()` non lanciano eccezioni sugli errori del provider — controlla tu `stopReason` e `errorMessage`, oppure tratterai silenziosamente i fallimenti come risultati.
- L'uso di classificatori e modelli immagine dentro uno script viene aggiunto al risultato del tool codemode e conta nel costo della sessione. Economico per chiamata non significa gratis a 150 chiamate.
- La generazione di immagini può richiedere minuti. Un `timeout_ms` breve su uno script che genera immagini lo ucciderà a metà volo.
Cache warming e la regola dei 0,05 $
Pi ha un'impostazione cacheWarming con tre valori — "off", "streaming"
(il default, che scalda durante le esecuzioni attive) e "idle" (anche tra le
esecuzioni).
La parte interessante è la condizione d'accesso. Il cache warming parte solo
quando il modello dichiara una durata della cache e Pi stima almeno 0,05 $
di costo di cache miss evitato. L'uso per il refresh conta nei totali della
sessione ma non entra nel contesto del modello. /session mostra la
prossima decisione, e un'estensione può sovrascriverla tramite un hook
cache_warming_decision.
Una soglia in dollari scritta a mano dentro un harness open source è un pezzo di ingegneria insolitamente onesto: scaldare una cache è a sua volta una richiesta fatturabile, quindi sotto un certo punto di pareggio è una tassa, non un'ottimizzazione. Se hai costruito il tuo loop di warming e non hai mai calcolato quella soglia, probabilmente la stai pagando. L'economia è la stessa di Economia del prompt caching.
Configurazione, e cosa sostituisce contro cosa aggiunge
La configurazione a livello utente vive nella directory dell'agente, per
default ~/.pi/agent (sovrascrivibile con PI_CODING_AGENT_DIR). La
configurazione di progetto vive in .pi/ sotto la directory di lavoro e si
carica solo dopo che la fiducia nel progetto è stata concessa.
| Percorso | Cosa fa |
|---|---|
<agent-dir>/settings.json | Preferenze, default, percorsi delle risorse, dichiarazioni dei pacchetti |
<agent-dir>/mcp.json, models.json, auth.json | Server MCP globali, endpoint compatibili e override, credenziali salvate |
<agent-dir>/SYSTEM.md | Sostituisce il system prompt di default di Pi |
<agent-dir>/APPEND_SYSTEM.md | Aggiunge al system prompt di Pi |
<agent-dir>/AGENTS.md — oppure CLAUDE.md | Istruzioni utente applicate in tutte le directory di lavoro |
<agent-dir>/extensions/, skills/, prompts/, themes/ | Risorse utente; prompts/ diventano slash command |
.pi/SYSTEM.md, .pi/APPEND_SYSTEM.md | Equivalenti di progetto; il file di progetto fidato vince su quello nella directory dell'agente — i file con lo stesso nome non vengono combinati |
Due cose da interiorizzare. Pi legge CLAUDE.md come file di contesto, quindi
un repo già configurato per Claude Code è già configurato per Pi — il punto sulla
portabilità da CLI per agenti di coding a confronto
vale in entrambe le direzioni. E AGENTS.override.md sostituisce AGENTS.md o
CLAUDE.md solo nella stessa directory; non sopprime i file di contesto
ereditati dalla directory dell'agente o dalle directory antenate.
I tool integrati sono read, bash, powershell, edit, write, grep,
find e ls, dei quali solo read, bash, edit e write sono attivi per
default. Le estensioni integrate sono builtin:mcp, builtin:llama.cpp,
builtin:codemode e builtin:tool-search — nota che llama.cpp è un
integrato di prima classe, quindi i pesi locali qui non sono un ripensamento.
Il modello di sicurezza, detto chiaramente
Il documento di sicurezza di Pi è rinfrescantemente diretto, e le tre frasi qui sotto sono quelle da prendere seriamente prima di puntarlo su un repo che non hai scritto tu.
- Pi non chiede approvazione prima di ogni chiamata a un tool. Estensioni, installatori di pacchetti e language server girano come processi figli con gli stessi permessi dell'account che ha avviato Pi. Le estensioni sono moduli TypeScript caricati nel processo di Pi — non sono isolate in sandbox, e la VM di codemode protegge il percorso di output del modello, non la tua macchina.
- La fiducia nel progetto non è un confine all'avvio. Pi legge l'impostazione `sessionDir` del progetto prima di risolvere la fiducia, quindi rifiutare la fiducia non può annullare quella lettura. Una directory `.pi` vuota non fa scattare affatto una richiesta di fiducia.
- La scoperta dei file di contesto non richiede la fiducia nel progetto. L'`AGENTS.md` o il `CLAUDE.md` di un repo appena clonato viene caricato nel system prompt senza chiedertelo — che è una superficie di prompt injection, non una comodità di configurazione.
- La documentazione lo dice esplicitamente: guardare il transcript, usare la fiducia nel progetto e rivedere le modifiche non creano un confine di sicurezza. Il confine è un container, una VM o un utente di sistema dedicato.
Se quest'ultimo punto è nuovo per te, leggi poi
Agenti di coding sotto attacco e
Mettere in sicurezza gli agenti locali e ibridi.
Una piccola mitigazione che Pi fa bene: il percorso di installazione npm è
documentato come
npm install -g --ignore-scripts @earendil-works/pi-coding-agent, e Pi non
richiede script di lifecycle delle dipendenze per un'installazione normale.
Cosa Pi deliberatamente non ha
| Assente | La posizione di Pi |
|---|---|
| Sub-agent | Non spediti. Chiedi a Pi di costruire quello che vuoi, o installa un pacchetto. |
| Plan mode | Non spedita. |
| Richieste di approvazione per singola chiamata | Non è il modello adottato. Il confine è l'isolamento, non i dialoghi di conferma. |
| Una dipendenza dall'SDK MCP ufficiale | pi-mcp è un client standalone senza dipendenze dall'SDK ufficiale né da altri pacchetti pi. |
| Un canale di contribuzione aperto | Issue e PR da nuovi contributori vengono chiuse automaticamente; i maintainer rivedono le submission chiuse ogni giorno. Leggi prima CONTRIBUTING.md. |
Le omissioni sono il prodotto. Un harness che rifiuta funzionalità finché non si dimostrano è un harness il cui budget di contesto resta leggibile — che è lo stesso argomento di Perché gli agenti bruciano token, fatto per sottrazione.
Per il trade-off opposto — un harness che ottimizza il numero di turni invece del contesto per turno — confronta Harness per agenti async-first, la cui esecuzione pubblicata su Terminal-Bench 4.0 è finita in 28 turni contro i 44 di Pi.
Pi Durable, in un respiro
@earendil-works/pi-durable è marcato sperimentale — l'API cambia senza
preavviso tra le release — ed è il pacchetto architetturalmente più interessante
del repo. Conversazioni, turni del modello, chiamate ai tool e il tuo stato
vengono committati su storage prima che qualcosa venga mostrato; se il processo
muore a metà turno, riaprire lo storage riprende da dove si era fermato.
Il vocabolario vale la pena rubarlo anche se non lo installerai mai: una
conversazione è un transcript di entry immutabili (pi.user,
pi.assistant, pi.tool-result, pi.system, pi.reset); i documenti sono
stato JSON tipizzato memorizzato accanto al transcript (pi.agent per le scelte
di modello, pi.live per la generazione in corso, pi.inbox per le submission
in coda, pi.usage per la spesa); un task è una macchina a stati durevole
che fa checkpoint a ogni passo, così pi.generation possiede i task pi.tool
delle sue chiamate ai tool e un processo riavviato continua dall'ultimo
checkpoint. E il modello vede solo le entry dal reset più recente in avanti —
il reset è una voce del transcript, non una cancellazione.
Confronta con Architetture di memoria per agenti e Harness per agenti a lunga esecuzione.
Dovresti passare a Pi?
Risposta onesta: probabilmente non in blocco, e va bene — le idee sono il risultato da portare a casa.
- Prova Pi se vuoi un solo harness per molti provider, usi pesi locali (llama.cpp è integrato), o la tua superficie MCP è cresciuta oltre la tua finestra di contesto.
- Resta dove sei se dipendi da sub-agent, plan mode o richieste di approvazione per singola chiamata. Pi non li ha e non se ne scusa.
- Ruba in ogni caso: la scala di esposizione (
codemode/deferred/direct/hidden), il trucco dell'aggiunta in coda per cambiare sezioni del prompt, e una soglia di pareggio misurata prima di scaldare qualsiasi cache.
Check yourself
0/6Fonti e approfondimenti
- Pi 1.0 — annuncio di Earendil (1 ottobre 2026) — il post di rilascio: codemode, caricamento differito dei tool, cache warming, messaggi di sistema a metà conversazione, Pi Durable.
- “You Said No MCP!” — Earendil — perché Pi ha resistito a MCP, l'obiezione sulla componibilità e cosa è cambiato.
- earendil-works/pi su GitHub — monorepo MIT;
packages/contiene agent, ai, chord, client, codemode, coding-agent, durable, evals, mcp, protocol, server, telemetry, tui. packages/coding-agent/docs/codemode.md— i globali, la riga di opzioni, la semantica dello store, l'interfacciamodels, i budget di output.packages/coding-agent/docs/mcp.md— la tabella delle esposizioni,toolExposure, l'aggiunta in coda che preserva la cache, il comportamento di connessione e retry.packages/coding-agent/docs/settings.md—codemode.inlineBudget,codemode.mode,cacheWarming,defaultTools, estensioni integrate.packages/coding-agent/docs/security.md— la tabella di isolamento, l'ambito della fiducia nel progetto e cosa esplicitamente non è un confine.packages/coding-agent/docs/configuration.md— layout della directory dell'agente e del.pidi progetto, scoperta dei file di contesto.packages/codemode/README.md— l'API standaloneCodemodeSandbox, i limiti di memoria, il reporting delle scritture nello store,CODEMODE_SOURCE_GRAMMAR.packages/durable/README.md— vocabolario di harness, conversation, entry, commit, document, task.packages/ai/README.md— l'elenco dei provider (inclusa l'API dei classificatori System One di TypeSafe e qualsiasi endpoint compatibile con OpenAI), risoluzione dell'autenticazione, hand-off tra provider.- pi.dev — script di installazione e documentazione renderizzata.
Prossimi passi
- CLI per agenti di coding a confronto — dove si colloca Pi rispetto a Claude Code, Codex CLI, opencode e Aider
- Quanto ti costano davvero i server MCP in token — misura nel tuo setup il problema che codemode risolve
- Harness per agenti async-first: cosa cambia Unreal Agent — l'altra metà dell'argomento sui costi: meno turni, non turni più piccoli