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>
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user