Files
ThothII/docs/superpowers/specs/2026-06-27-backend-design.md
T
marcopanandClaude Opus 4.8 aad6299c99 docs(backend): design del backend ThothII (brainstorming)
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>
2026-06-27 18:09:51 +02:00

18 KiB
Raw Blame History

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)

  1. FE → POST /sessions {workspace, question, provider?, model?, thinking?, name?}.
  2. Backend esegue tht session new → ottiene <id> e la directory di sessione; scrive provider/model/thinking/name nel manifest (BE-6/BE-7).
  3. Backend fa spawn di pi --mode rpc (cwd=harness, --approve, --provider/--model/--thinking/--name, --session-dir/--session-id per agganciare la sessione) e inietta l'id nel kickoff /nuova-domanda.
  4. Il modello carica la skill tht-sessione e avvia F1; il gate emette il primo widget come extension_ui_request (JSONL su stdout).
  5. RpcClient lo riceve → SessionBridge lo traduce in ui_request → SSE hub lo manda al FE; la ui_request resta tracciata come "pendente".
  6. FE renderizza il widget, l'utente risponde → POST /sessions/:id/response {ui_response}.
  7. Backend traduce in extension_ui_response e lo scrive su stdin di Pi; il gate prosegue, registra la decisione via tht, 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 lancia tht sql preview --json (con offset) → righe per AGGrid.
  • POST /sessions/:id/sql/export → backend lancia tht 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

  1. 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 che tht sia 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.
  2. RpcClient (uno per Pi) — LineSplitter LF-only su stdout (no readline, split su \n), parse JSONL, dispatch per id, scrittura JSONL su stdin. Framing derivato dal RpcClient/LineSplitter di riferimento (rischio fake-Pi: il fake deve riprodurre fedelmente questo framing).
  3. SessionBridge (traduttore) — extension_ui_request → ui_request, ui_response → extension_ui_response; passthrough di info/text_delta/system_event. Mantiene la ui_request pendente per sessione (re-emit, BE-3).
  4. SSE hub — uno stream per sessione; re-emit del widget pendente alla (ri)sottoscrizione.
  5. 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.
  6. Auth middleware — none/mock/oidc (D6).
  7. 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 di tht, 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 rpc reale, 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:

  1. tht sql preview: output --json strutturato + supporto --offset (paginazione AGGrid). (Verificare lo stato attuale: oggi preview ha --limit ma non --offset/--json esplicito.)
  2. Kickoff/gate: accettare un session id fornito invece di farlo creare al modello (BE-5).
  3. session_manifest.yaml: nuovi campi provider, model, thinking, name (BE-6/BE-7), oltre ai campi ThothII già previsti (author, ecc.).
  4. .pi/settings.json: quietStartup: true + trust configurato per i file project-local (BE-7).
  5. tht session list/show --json: per la lista/dettaglio sessioni nel FE (verificare se già presente).
  6. 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 tht persistente 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 tht nel processo Pi spawnato (bug #1 dell'L2): il gate chiama execFileSync("tht", …); il tht del venv deve essere raggiungibile dall'ambiente del child. Mitigazione: il PiProcessManager imposta PATH/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/trust pre-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/LineSplitter di riferimento.
  • Dipendenze harness (§7) incomplete: senza preview --json/--offset e 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.