Decisioni BE-1..BE-7: un Pi per sessione attiva, SQL finale delegato a tht (codepath unico, rischio D7 eliminato), resilienza via ricostruzione da disco + re-emit del widget pendente, test con fake-Pi condiviso, backend pre-crea la sessione, model/thinking/provider per-sessione persistiti, settings Pi MVP. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
18 KiB
ThothII — Design del backend
Data: 2026-06-27
Stato: Draft, in attesa di review
Fonti: docs/superpowers/specs/2026-06-25-thothii-architecture-design.md (architettura d'insieme), docs/l2-run-report-2026-06-27.md (primo run RPC end-to-end), codice harness/ (CLI tht + gate tht-gate.js), pi --help di @earendil-works/pi-coding-agent.
1. Obiettivo e contesto
L'harness di ThothII è completo e validato (L0/L1 deterministici + un run L2 reale con GLM 5.2). Il report L2 del 2026-06-27 ha messo a fuoco il gap che questo progetto colma:
"La modalità RPC emette i widget come JSONL su stdio per un client esterno — che non esiste ancora in ThothII."
Il backend È quel client esterno mancante. È il componente che rende l'harness usabile end-to-end: avvia Pi in modalità RPC, fa da ponte tra il protocollo JSONL del gate e il frontend (SSE + REST), e delega a tht l'esecuzione controllata del SQL finale.
Questo documento è autonomo ma referenzia l'architettura d'insieme per i contratti condivisi: il widget-descriptor (spec architetturale §4) e i modelli dati di sessione/workspace (spec §5). Non li ridefinisce.
Posizione nel piano generale: secondo dei tre progetti (harness → backend → frontend), come previsto da D1/D9 e §11 dell'architettura. Un solo piano di implementazione per il backend, costruito a slice incrementali con un primo loop end-to-end F1 il prima possibile.
2. Decisioni del backend (locked)
Decise durante il brainstorming del 2026-06-27. Ogni voce riporta la scelta e il perché.
BE-1 — Un processo Pi per sessione attiva.
Il backend fa spawn di un processo pi --mode rpc (cwd = harness/) per ogni sessione attiva (aperta dall'operatore). Riprendere una sessione = respawn puntato sull'id esistente; il ledger su disco (review_decisions.jsonl) è la verità, quindi lo stato sopravvive al teardown del processo.
Perché: isolamento totale tra sessioni, coerente con il modello a sessioni multiple del FE. L'MVP è mono-operatore localhost (D12-B), quindi il numero di processi concorrenti è basso e gestibile con un cap di sicurezza (vedi §5).
BE-2 — Esecuzione del SQL finale delegata a tht (codepath unico).
Il backend non si collega mai direttamente al DB. Per alimentare AGGrid chiama tht sql preview (con paginazione), per l'export chiama tht sql export. Riusa enforcement read-only, execution.allow, transport (direct/rest) e gestione dialetto già esistenti e testati in tht.
Perché: elimina il rischio architetturale D7 (due codepath SQL da tenere allineate sul read-only). Lo spec architetturale lo scelse come rischio aperto; qui lo chiudiamo. Costo misurato: ~190 ms di overhead per chiamata (cold-start dell'interprete Python), perché il round-trip al DWH (~40–80 ms via REST) è identico nei due approcci. Irrilevante per l'export, tollerabile per la paginazione di uno strumento di review, abbattibile post-MVP con un worker tht persistente se mai servisse.
Questa è una deviazione esplicita da D7 ("il backend si collega al DB del workspace"): il backend resta un orchestratore/traduttore puro, senza client DB proprio.
BE-3 — Resilienza: ricostruzione da disco + re-emit del widget pendente.
Al (ri)caricamento il FE ricostruisce lo stato della sessione via REST (ledger + artefatti su disco = verità). Alla (ri)sottoscrizione SSE il backend ri-emette solo la ui_request attualmente pendente (quella che il modello sta aspettando), che già traccia per la correlazione. Nessun buffer/event-store. I text_delta persi durante un disconnect non si recuperano (accettabile: conta l'artefatto/widget finale). Un restart del backend termina i processi Pi figli; al resume di una sessione, Pi viene respawnato.
Perché: il modello più semplice che copre i casi reali del mono-operatore (chiudo/riapro tab, riavvio backend, crash Pi) senza introdurre un event log con retention da gestire (over-engineering per l'MVP).
BE-4 — Test: fake-Pi condiviso + unit test TS.
Si costruisce un fake-Pi: uno script che parla il protocollo RPC di Pi su stdio, scriptato per emettere sequenze fisse di extension_ui_request. Serve sia i test d'integrazione del backend (spawn + stdio + framing LF-only + correlazione reali) sia i golden test dell'harness (colma il gap D10 segnalato dall'L2). In più, unit test TS sulla logica pura di traduzione/correlazione. Il livello L2 con Pi reale resta separato e informativo (non-deterministico, richiede VPN/credenziali).
Perché: l'unica strategia che testa in modo deterministico la parte più fragile (framing stdio + correlazione) senza LLM né rete in CI.
BE-5 — Il backend pre-crea la sessione e possiede l'id.
Su POST /sessions il backend esegue lui tht session new (conosce subito id e directory), poi fa spawn di Pi iniettando l'id già creato nel kickoff (/nuova-domanda). Il modello usa la sessione, non la crea.
Perché: l'alternativa (il modello crea la sessione, il backend ne "scopre" l'id osservando la dir o parsando l'output) è fragile e soggetta a race. Backend padrone dell'id e del ciclo di vita. Richiede una piccola modifica al kickoff/gate dell'harness (id fornito invece che creato dal modello) — vedi §7.
BE-6 — model/thinking/provider per-sessione, persistiti, riapplicati al resume.
POST /sessions accetta {provider?, model?, thinking?} opzionali; in mancanza usa i default da config del backend. I tre valori si persistono nel manifest di sessione e si riapplicano al respawn in fase di resume. Niente cambio a sessione in corso nell'MVP.
Perché: copre il caso d'uso principale ("scelgo il modello giusto per questa domanda") in modo deterministico. Pi supporta nativamente --provider/--model/--thinking componibili con --mode rpc (verificato).
BE-7 — Settings Pi esposti nell'MVP.
Esposti al FE (per-sessione, hanno flag CLI): --name (nome visualizzato sessione). Globali via config operatore (.pi/settings.json): temperature (bassa di default per determinismo NL→SQL), maxTokens (cap output, legato al budget contesto del 35B, D16). Endpoint GET /models (via pi --list-models) per la tendina FE. Correttezza dello spawn (non opzionali): trust/--approve per fidare i file project-local (gate extension + skill) ed evitare un prompt di trust che appenderebbe il loop RPC; quietStartup: true per non sporcare il flusso JSONL letto dal RpcClient; systemPrompt mai sovrascritto (il comportamento è guidato da kickoff + skill; un override romperebbe il gate).
Perché: temperature, maxTokens, quietStartup, trust, systemPrompt, tools non hanno flag CLI in Pi → vivono in .pi/settings.json (globali al progetto), non sono per-sessione. Limite noto: temperature/maxTokens per-sessione non sono possibili oggi senza un flag che Pi non espone.
3. Architettura e flusso dei dati
Il backend è un orchestratore/traduttore senza stato persistente proprio: la verità sta su disco (ledger + artefatti dell'harness). In RAM tiene, per ogni sessione attiva, solo l'handle del processo Pi e la ui_request pendente. Stack: Node.js + Fastify + TypeScript. Deployment MVP: localhost, mono-operatore (D12-B).
┌─ FRONTEND (frontend/) ──────────────────────────────────────────────┐
│ React/Next/ShadCn/AGGrid · consuma SOLO la REST+SSE del backend │
└──────────────────────────────▲──────────────────────────────────────┘
│ HTTP/SSE (JSON, localhost)
┌─ BACKEND (backend/) ──────────┴──────────────────────────────────────┐
│ Node + Fastify + TS │
│ • PiProcessManager: un `pi --mode rpc` per sessione attiva (BE-1) │
│ • RpcClient (per Pi): LineSplitter LF-only + dispatch per id │
│ • SessionBridge: extension_ui_request ↔ ui_request, │
│ ui_response ↔ extension_ui_response │
│ • SSE hub: stream per sessione, re-emit del widget pendente (BE-3) │
│ • Auth middleware: none(MVP) | mock | oidc (D6) │
│ • Delega SQL: tht sql preview/export (BE-2) — NESSUN client DB │
│ • REST: workspaces, sessions, artifacts, models │
└────────────────────────────────┬──────────────┬─────────────────────┘
JSONL (LF-only)│ │ subprocess `tht … --json`
▼ ▼
┌─ pi --mode rpc ─┐ ┌─ tht (CLI Python) ─┐
│ gate tht-gate.js│ │ session/sql/phase │
│ emette widget │ │ … --json │
└─────────────────┘ └────────────────────┘
│ │
└── entrambi leggono ──┘
harness/sessions/<id>/ (verità)
Flusso di una nuova domanda (BE-5)
- FE →
POST /sessions {workspace, question, provider?, model?, thinking?, name?}. - Backend esegue
tht session new→ ottiene<id>e la directory di sessione; scrive provider/model/thinking/name nel manifest (BE-6/BE-7). - Backend fa spawn di
pi --mode rpc(cwd=harness,--approve,--provider/--model/--thinking/--name,--session-dir/--session-idper agganciare la sessione) e inietta l'id nel kickoff/nuova-domanda. - Il modello carica la skill
tht-sessionee avvia F1; il gate emette il primo widget comeextension_ui_request(JSONL su stdout). - RpcClient lo riceve → SessionBridge lo traduce in
ui_request→ SSE hub lo manda al FE; laui_requestresta tracciata come "pendente". - FE renderizza il widget, l'utente risponde →
POST /sessions/:id/response {ui_response}. - Backend traduce in
extension_ui_responsee lo scrive su stdin di Pi; il gate prosegue, registra la decisione viatht, la fase deriva.
Flusso del SQL finale (BE-2)
A workflow concluso (sql_final.sql approvato), il FE richiede i risultati:
POST /sessions/:id/sql/preview?limit=&offset=→ backend lanciatht sql preview --json(con offset) → righe per AGGrid.POST /sessions/:id/sql/export→ backend lanciatht sql export→ file CSV in download.
4. API REST + SSE (contratto FE↔BE)
Tutti i payload ui_request/ui_response/info/system_event seguono il widget-descriptor dell'architettura §4 (non ridefinito qui). Il backend è trasporto puro per quei messaggi.
| Metodo | Path | Scopo |
|---|---|---|
GET |
/workspaces |
Lista workspace (lettura da harness/workspaces/*.yaml, read-only, no CRUD — D3) |
GET |
/models |
Modelli disponibili (via pi --list-models) per la tendina FE |
POST |
/sessions |
Crea sessione: {workspace, question, provider?, model?, thinking?, name?} → {id}. Backend pre-crea + spawn Pi (BE-5/BE-6) |
GET |
/sessions |
Lista sessioni (da disco, via tht session list --json o FS) |
GET |
/sessions/:id |
Manifest + fase derivata (via tht … --json) |
GET |
/sessions/:id/artifacts/* |
Artefatti (schema_linking, ctes, sql_final, …) via tht --json/FS |
GET |
/sessions/:id/events |
SSE: stream eventi (text_delta, ui_request, info, system_event, lifecycle). Re-emit del widget pendente alla (ri)sottoscrizione (BE-3) |
POST |
/sessions/:id/response |
Invia ui_response → extension_ui_response su stdin Pi |
POST |
/sessions/:id/steer |
Testo libero (canale steering !, architettura §4.3) → stdin Pi |
POST |
/sessions/:id/sql/preview |
?limit=&offset= → delega tht sql preview --json → AGGrid (BE-2) |
POST |
/sessions/:id/sql/export |
Delega tht sql export → CSV (BE-2) |
POST |
/sessions/:id/close |
Teardown del processo Pi (la sessione su disco resta) |
Auth (D6): middleware pluggabile none (utente dev@local, primaria nell'MVP B) | mock (utente statico da header, test) | oidc (evoluzione ad A). L'utente autenticato alimenta il campo author alla creazione sessione. Una sola codepath.
5. Componenti interni
- PiProcessManager — ciclo di vita dei processi Pi: spawn (new/resume) con i flag corretti (
--mode rpc,--approve,--provider/--model/--thinking/--name,--session-dir/--session-id), garanzia chethtsia nel PATH del child (fix bug #1 dell'L2), teardown alla chiusura/idle, cap di sicurezza sul numero di processi concorrenti, timeout di spawn. Al resume rilegge provider/model/thinking dal manifest e respawna con gli stessi. - RpcClient (uno per Pi) —
LineSplitterLF-only su stdout (noreadline, split su\n), parse JSONL, dispatch perid, scrittura JSONL su stdin. Framing derivato dalRpcClient/LineSplitterdi riferimento (rischio fake-Pi: il fake deve riprodurre fedelmente questo framing). - SessionBridge (traduttore) —
extension_ui_request → ui_request,ui_response → extension_ui_response; passthrough diinfo/text_delta/system_event. Mantiene laui_requestpendente per sessione (re-emit, BE-3). - SSE hub — uno stream per sessione; re-emit del widget pendente alla (ri)sottoscrizione.
- ThtRunner — wrapper per le invocazioni
tht … --json(session new/list/show, sql preview/export, artifacts): gestione subprocess, parsing JSON, mappatura degli exit-code del CLI. - Auth middleware —
none/mock/oidc(D6). - Config — posizione dell'harness, dir workspace, profilo, porta, default per-sessione (provider/model/thinking), cap processi, timeout.
6. Strategia di test (BE-4)
- L1 unit (TS): logica pura di SessionBridge (traduzione widget-descriptor ↔ RPC), correlazione per
id, parsing degli exit-code ditht, mappatura auth. Nessun subprocess. - L1 integrazione (fake-Pi): il backend fa spawn del fake-Pi (asset condiviso) e si verifica il bridge reale: spawn, stdio, framing LF-only, correlazione, re-emit del pendente, sequenze multi-widget. Deterministico, CI-friendly.
- L2 (informativo): end-to-end contro
pi --mode rpcreale, separato, non in CI (LLM non-deterministico, richiede VPN/credenziali). Coerente con lo split L0/L1/L2 dell'harness.
Il fake-Pi è progettato per essere riusato dai golden test dell'harness (D10), così esiste un'unica fonte di fedeltà del protocollo.
7. Dipendenze verso l'harness
Il bridge non chiude il loop senza queste modifiche/verifiche lato harness/, da far atterrare prima o insieme al backend:
tht sql preview: output--jsonstrutturato + supporto--offset(paginazione AGGrid). (Verificare lo stato attuale: oggipreviewha--limitma non--offset/--jsonesplicito.)- Kickoff/gate: accettare un session id fornito invece di farlo creare al modello (BE-5).
session_manifest.yaml: nuovi campiprovider,model,thinking,name(BE-6/BE-7), oltre ai campi ThothII già previsti (author, ecc.)..pi/settings.json:quietStartup: true+trustconfigurato per i file project-local (BE-7).tht session list/show --json: per la lista/dettaglio sessioni nel FE (verificare se già presente).- fake-Pi condiviso: asset di test (BE-4), colma anche il gap D10.
8. Fuori scope (MVP)
- Web app centrale multi-utente (modello A, D12): l'MVP è B (localhost). L'evoluzione ad A riposiziona backend+harness su server e attiva OIDC senza cambiare i contratti.
- Concorrenza multi-utente reale (l'MVP è mono-operatore; il cap processi è solo una salvaguardia).
- Job runner asincrono: preview/export sono sincroni (coerente con l'architettura §9).
- CRUD workspace via API (i workspace sono YAML, scrittura manuale; il backend li espone in lettura — D3).
- Event-log/replay SSE con Last-Event-ID (BE-3 sceglie il re-emit del pendente).
- Cambio di model/thinking a sessione in corso (BE-6: solo alla creazione).
- Worker
thtpersistente per azzerare l'overhead di delega (BE-2: si valuta solo se i ~190 ms/chiamata diventano un problema reale).
9. Rischi aperti
- PATH di
thtnel processo Pi spawnato (bug #1 dell'L2): il gate chiamaexecFileSync("tht", …); ilthtdel venv deve essere raggiungibile dall'ambiente del child. Mitigazione: il PiProcessManager impostaPATH/usa path assoluto. - Trust dei file project-local (BE-7): se Pi mostra un prompt di trust per la gate extension/skill, in RPC mode il loop si appende. Mitigazione:
--approve/trustpre-configurato; da verificare empiricamente al primo spawn. - Versione di Pi (
@earendil-works/pi-coding-agent): il protocollo RPC e i flag possono cambiare. Mitigazione: pinnare la versione e documentarla. - Fedeltà del fake-Pi (rischio ereditato dall'architettura): se il fake devia dal framing reale (LF-only, JSONL), i golden test non catturano regressioni reali. Mitigazione: basarlo sul
RpcClient/LineSplitterdi riferimento. - Dipendenze harness (§7) incomplete: senza
preview --json/--offsete l'id iniettato nel kickoff il loop non chiude. Mitigazione: trattarle come prerequisiti espliciti del piano.
10. Nota sul piano di implementazione
Questo documento è il design del backend. La fase di writing-plans produrrà il piano di implementazione del backend (uno, come da §11 dell'architettura), costruito a slice incrementali: prima il bridge RPC + SSE + REST minimale per chiudere il loop F1 end-to-end (con fake-Pi), poi steering, artifacts, delega SQL (preview/export), auth e i settings Pi. I contratti FE↔BE definiti qui (§4) e il widget-descriptor dell'architettura (§4) permettono di implementare e testare il backend in isolamento contro il contratto.