mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-17 12:16:21 +02:00
Port de codeVerifyLoop (rust/loki-rs/src/chat/verify.rs) : à la fin d'un tour de build en mode Code, relances du builder sur les critères encore ouverts, passe de vérification sur un contexte isolé, corrections, abandon dit dans le fil après deux corrections — mêmes deltas que le Go (role, verify_done, mesures ctx_isolated), relances et corrections rangées dans l'historique, vérifications non. Le Go garde les critères (l'outil criteria s'y exécute) : la façade les lit en base (chat/crit:<id>) et reçoit le contexte isolé du vérificateur par POST /internal/v1/turn/verifier (prompt du rôle et faits de la machine, outils fermés, rôle verifier, seul autorisé à marquer passed). Toute compaction en mode Code rend l'état des critères au résumé, comme compactMessages. generate : vue du tour factorisée (turn_view), question à l'utilisateur suivie (une question posée suspend la vérification). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
196 lines
6.5 KiB
Go
196 lines
6.5 KiB
Go
package loki
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// API interne d'un tour de discussion tenu par la façade Rust (lot 6).
|
|
//
|
|
// La boucle, le journal et la persistance sont en Rust ; ce qui décrit le
|
|
// tour reste ici, le temps d'être porté (lot 7) : pièces jointes et vision,
|
|
// rôle du mode Code, prompt du preset, consignes du dépôt, contexte du
|
|
// projet, préambule d'agent, outils (MCP compris), puce « mode Code »,
|
|
// notification Web Push, et l'état par discussion que les outils lisent.
|
|
//
|
|
// POST /internal/v1/turn/start corps de /api/chat/send
|
|
// → {files, caps, turn_caps, content, model, code_hint}
|
|
// POST /internal/v1/turn/view {"caps":{…}} → {head, prefix, tools}
|
|
// 409 : PROJ_SNAPSHOT actif (le bloc projet figé reste au Go)
|
|
// POST /internal/v1/turn/done {"ms":N} → 204 (Web Push si abonnés)
|
|
// POST /internal/v1/turn/verifier {"caps":{…}} → {system, tools, caps}
|
|
// (passe de vérification du mode Code : contexte ISOLÉ, rôle verifier)
|
|
// POST /internal/v1/conv/active {"id":…} → 204 (discussion active des outils)
|
|
// POST /internal/v1/conv/dropped {"id":…} → 204 (fichiers, jobs, critères,
|
|
// mode, résultats d'outils ;
|
|
// "files_only" : les fichiers)
|
|
|
|
const internalTurnMaxBody = 8 << 20
|
|
|
|
func readInternalJSON(w http.ResponseWriter, r *http.Request, v any) bool {
|
|
if r.Method != http.MethodPost {
|
|
w.Header().Set("Allow", http.MethodPost)
|
|
sendJSON(w, http.StatusMethodNotAllowed, map[string]any{"error": "POST attendu"})
|
|
return false
|
|
}
|
|
raw, err := io.ReadAll(io.LimitReader(r.Body, internalTurnMaxBody+1))
|
|
if err != nil || len(raw) > internalTurnMaxBody || json.Unmarshal(raw, v) != nil {
|
|
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "corps illisible"})
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// handleInternalTurnStart : ce que StartTurn et EnqueueOrStart tirent de la
|
|
// requête avant de lancer (ou de mettre en file) le tour. caps : capacités
|
|
// demandées (file d'attente, préchauffage) ; turn_caps : celles du tour, rôle
|
|
// du mode Code figé ; content : le message utilisateur vu par le modèle.
|
|
func handleInternalTurnStart(w http.ResponseWriter, r *http.Request) {
|
|
var body chatReq
|
|
if !readInternalJSON(w, r, &body) {
|
|
return
|
|
}
|
|
files := attachFiles(body.Files)
|
|
if strings.TrimSpace(body.Message) == "" && len(files) == 0 {
|
|
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "message vide"})
|
|
return
|
|
}
|
|
rememberUserTZ(body.TZ)
|
|
caps := capsFromBody(body)
|
|
turnCaps := withTurnRole(caps, body.Message)
|
|
prompt := body.Message
|
|
if strings.TrimSpace(prompt) == "" {
|
|
prompt = "Prends-en connaissance."
|
|
}
|
|
if files == nil {
|
|
files = []attachInfo{}
|
|
}
|
|
sendJSON(w, http.StatusOK, map[string]any{
|
|
"files": files,
|
|
"caps": caps,
|
|
"turn_caps": turnCaps,
|
|
"content": userMessageContent(files, prompt),
|
|
"model": chatModelName(),
|
|
"code_hint": !turnCaps.Code && codeHintDue(body.Message),
|
|
})
|
|
}
|
|
|
|
// handleInternalTurnView : les pièces de turnView (assembleTurn sans
|
|
// PROJ_SNAPSHOT) que la façade assemble autour de l'historique : head part
|
|
// devant les messages, puis prefix (InjectSkills) fusionné dans le premier
|
|
// système qui n'est pas un message projet, ou posé en tête.
|
|
func handleInternalTurnView(w http.ResponseWriter, r *http.Request) {
|
|
var in struct {
|
|
Caps Caps `json:"caps"`
|
|
}
|
|
if !readInternalJSON(w, r, &in) {
|
|
return
|
|
}
|
|
caps := in.Caps
|
|
if caps.Agent && projSnapEnabled(ReadConfig()) {
|
|
sendJSON(w, http.StatusConflict, map[string]any{"error": "PROJ_SNAPSHOT actif : le bloc projet figé reste tenu par le Go"})
|
|
return
|
|
}
|
|
head := []Message{}
|
|
if caps.Agent {
|
|
if sp := readSysPrompt(); sp != "" {
|
|
head = append(head, Message{Role: "system", Content: sp})
|
|
}
|
|
if m, ok := codeInstructionsMessage(caps); ok {
|
|
head = append(head, m)
|
|
}
|
|
head = append(head, projectSystemMessages()...)
|
|
}
|
|
tools := EnabledTools(caps)
|
|
if tools == nil {
|
|
tools = []Tool{}
|
|
}
|
|
sendJSON(w, http.StatusOK, map[string]any{"head": head, "prefix": injectPrefix(caps, tools), "tools": tools})
|
|
}
|
|
|
|
// handleInternalTurnVerifier : ce que runVerifyPass envoie au moteur hors de
|
|
// l'historique — le prompt du rôle verifier suivi des faits de la machine, et
|
|
// le jeu d'outils fermé de la vérification. caps : celles du tour, rôle
|
|
// remplacé (le Go s'y fie pour laisser l'outil criteria marquer « passed »).
|
|
func handleInternalTurnVerifier(w http.ResponseWriter, r *http.Request) {
|
|
var in struct {
|
|
Caps Caps `json:"caps"`
|
|
}
|
|
if !readInternalJSON(w, r, &in) {
|
|
return
|
|
}
|
|
vcaps := in.Caps
|
|
vcaps.Role = "verifier"
|
|
sendJSON(w, http.StatusOK, map[string]any{
|
|
"system": rolePrompt("verifier") + "\n\n" + machineSystemPrompt(vcaps),
|
|
"tools": EnabledTools(vcaps),
|
|
"caps": vcaps,
|
|
})
|
|
}
|
|
|
|
func handleInternalTurnDone(w http.ResponseWriter, r *http.Request) {
|
|
var in struct {
|
|
Ms int64 `json:"ms"`
|
|
}
|
|
if !readInternalJSON(w, r, &in) {
|
|
return
|
|
}
|
|
if hasPushSubs() {
|
|
d := time.Duration(in.Ms) * time.Millisecond
|
|
go sendPushToAll("Loki", "Réponse prête · "+fmtDurFR(d))
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// handleInternalConvActive : la façade a changé de discussion active. Les
|
|
// outils exécutés ici (dossier de la discussion, mode Code, critères) lisent
|
|
// l'identifiant en cache : il suit.
|
|
func handleInternalConvActive(w http.ResponseWriter, r *http.Request) {
|
|
var in struct {
|
|
ID string `json:"id"`
|
|
}
|
|
if !readInternalJSON(w, r, &in) {
|
|
return
|
|
}
|
|
if strings.TrimSpace(in.ID) == "" {
|
|
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "id manquant"})
|
|
return
|
|
}
|
|
convActiveMu.Lock()
|
|
convActiveRemember(in.ID)
|
|
convActiveMu.Unlock()
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// handleInternalConvDropped : ce qui part avec une discussion supprimée par
|
|
// la façade (mêmes effets que la fin de convDelete).
|
|
func handleInternalConvDropped(w http.ResponseWriter, r *http.Request) {
|
|
var in struct {
|
|
ID string `json:"id"`
|
|
// FilesOnly : seulement les fichiers (Reset vide la discussion, il ne
|
|
// la supprime pas).
|
|
FilesOnly bool `json:"files_only"`
|
|
}
|
|
if !readInternalJSON(w, r, &in) {
|
|
return
|
|
}
|
|
if strings.TrimSpace(in.ID) == "" {
|
|
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "id manquant"})
|
|
return
|
|
}
|
|
if in.FilesOnly {
|
|
dropConvFiles(in.ID)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
stopConvJobs(in.ID)
|
|
critDrop(in.ID)
|
|
dropConvMode(in.ID)
|
|
dropConvFiles(in.ID)
|
|
deleteToolResultsFor(in.ID)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|