mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-20 21:48:47 +02:00
Lot 32 : images gardées (KEEP_TURN_IMAGES) dans le tour de la façade
KEEP_TURN_IMAGES ne garde plus la conversation au Go. L'état du tour vit dans la façade ; les fichiers et la vision restent au Go. Go (facade_keepimg.go) : - /internal/v1/turn/keepimg : vision réellement active, budget des images gardées ; - /internal/v1/turn/keepimg/store : une image relayée rangée par référence (refRelayImage), relue à l'octet près. Rust : - chat/keepimg.rs : port de keepImages (image à l'essai, mesure par le moteur répartie au prorata des octets, budget, publication, fin de tour). - La boucle du tour : budget demandé pour le fil principal, image rangée par le Go, historique réécrit publié sans les relais non gardés ; les images gardées partent avant tout résumé (filet réactif, en cours de tour, fenêtre pleine). - Début et fin de tour : retirées si le contexte le demande ou si elles ne doivent plus partir (clé retirée, vision absente). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
17 files changed
+1435
-37
No files matched your search
@@ -48,17 +48,16 @@ func facadeChat() bool {
|
||||
|
||||
// facadeChatBlocker : le réglage qui garde la conversation ici, "" s'il n'y
|
||||
// en a pas. Ces clés opt-in demandent une machinerie restée au Go (bloc projet
|
||||
// figé, images gardées, préchauffage). NUDGE_IN_TOOL n'en est plus (lot 23) :
|
||||
// la façade a sa sonde de gabarit ; ni les motifs de relance personnalisés
|
||||
// (retry_patterns, lot 30) : la façade les lit et les compile ; ni SIDE_SLOT et
|
||||
// SLOT_PERSIST (lot 31) : la façade demande son slot avant chaque requête
|
||||
// (facade_slots.go).
|
||||
// figé, préchauffage). NUDGE_IN_TOOL n'en est plus (lot 23) : la façade a sa
|
||||
// sonde de gabarit ; ni les motifs de relance personnalisés (retry_patterns,
|
||||
// lot 30) : la façade les lit et les compile ; ni SIDE_SLOT et SLOT_PERSIST
|
||||
// (lot 31) : la façade demande son slot avant chaque requête
|
||||
// (facade_slots.go) ; ni KEEP_TURN_IMAGES (lot 32) : la façade tient l'état
|
||||
// du tour, le Go range les images (facade_keepimg.go).
|
||||
func facadeChatBlocker(cfg map[string]string) string {
|
||||
switch {
|
||||
case projSnapEnabled(cfg):
|
||||
return "PROJ_SNAPSHOT"
|
||||
case keepTurnImagesOn(cfg):
|
||||
return "KEEP_TURN_IMAGES"
|
||||
case prewarmMode(cfg) != "":
|
||||
return "PREWARM"
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ func TestFacadeReglageQuiGardeLaConversation(t *testing.T) {
|
||||
t.Fatalf("détenteur : %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
// Posé après la décision : sans effet avant le redémarrage.
|
||||
if err := SetConfigKey("KEEP_TURN_IMAGES", "on"); err != nil {
|
||||
if err := SetConfigKey("PREWARM", "on"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !facadeChat() {
|
||||
@@ -285,10 +285,10 @@ func TestFacadeReglageQuiGardeLaConversation(t *testing.T) {
|
||||
}
|
||||
oublierDecisionFacade(t)
|
||||
if facadeChat() {
|
||||
t.Fatal("KEEP_TURN_IMAGES posé : la conversation doit rester au Go")
|
||||
t.Fatal("PREWARM posé : la conversation doit rester au Go")
|
||||
}
|
||||
w := appelOAIInterne(t, "GET", "/internal/v1/chat/holder", "")
|
||||
if !strings.Contains(w.Body.String(), `"holder":"go"`) || !strings.Contains(w.Body.String(), "KEEP_TURN_IMAGES") {
|
||||
if !strings.Contains(w.Body.String(), `"holder":"go"`) || !strings.Contains(w.Body.String(), "PREWARM") {
|
||||
t.Fatalf("détenteur : %s", w.Body.String())
|
||||
}
|
||||
local := func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "go") }
|
||||
@@ -299,7 +299,7 @@ func TestFacadeReglageQuiGardeLaConversation(t *testing.T) {
|
||||
}
|
||||
for k, want := range map[string]string{
|
||||
"PROJ_SNAPSHOT": "PROJ_SNAPSHOT", "NUDGE_IN_TOOL": "", "SIDE_SLOT": "",
|
||||
"SLOT_PERSIST": "", "PREWARM": "PREWARM",
|
||||
"SLOT_PERSIST": "", "KEEP_TURN_IMAGES": "", "PREWARM": "PREWARM",
|
||||
} {
|
||||
if got := facadeChatBlocker(map[string]string{k: "on"}); got != want {
|
||||
t.Errorf("%s : %q", k, got)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// facade_keepimg.go — KEEP_TURN_IMAGES pour le tour tenu par la façade (lot 32
|
||||
// de la migration). L'état du tour (images à l'essai, mesure, budget) vit dans
|
||||
// la façade ; ce qui touche aux fichiers et à la vision reste ici :
|
||||
//
|
||||
// - /internal/v1/turn/keepimg : la vision est-elle réellement active
|
||||
// (visionEnabled, engineSeesImages) et le budget des images gardées
|
||||
// (keepImgBudget) ;
|
||||
// - /internal/v1/turn/keepimg/store : une image relayée rangée par
|
||||
// référence (refRelayImage : fichier chiffré si la mémoire l'est, date
|
||||
// rafraîchie, relu à l'octet près).
|
||||
|
||||
// handleInternalTurnKeepImg : POST {} → {"vision":bool,"budget":n}.
|
||||
func handleInternalTurnKeepImg(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct{}
|
||||
if !readInternalJSON(w, r, &in) {
|
||||
return
|
||||
}
|
||||
sendJSON(w, http.StatusOK, map[string]any{
|
||||
"vision": visionEnabled() && engineSeesImages(),
|
||||
"budget": keepImgBudget(),
|
||||
})
|
||||
}
|
||||
|
||||
// handleInternalTurnKeepImgStore : POST {"msg":Message} →
|
||||
// {"ok":true,"msg":Message,"weight":octets} ou {"ok":false} (format inconnu,
|
||||
// disque plein, mémoire verrouillée, relecture différente : l'image reste
|
||||
// éphémère).
|
||||
func handleInternalTurnKeepImgStore(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
Msg Message `json:"msg"`
|
||||
}
|
||||
if !readInternalJSON(w, r, &in) {
|
||||
return
|
||||
}
|
||||
m, weight, ok := refRelayImage(in.Msg)
|
||||
if !ok {
|
||||
sendJSON(w, http.StatusOK, map[string]any{"ok": false})
|
||||
return
|
||||
}
|
||||
sendJSON(w, http.StatusOK, map[string]any{"ok": true, "msg": m, "weight": weight})
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// L'état que la façade demande au début d'un tour : vision réellement active
|
||||
// (projecteur déclaré ET vu par le moteur) et budget des images gardées.
|
||||
func TestFacadeKeepImgEtat(t *testing.T) {
|
||||
testHome(t)
|
||||
t.Setenv(internalTokenEnv, jetonTest)
|
||||
if err := SetConfigKey("CTX", "2000"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
etat := func() (bool, float64) {
|
||||
w := appelOAIInterne(t, "POST", "/internal/v1/turn/keepimg", `{}`)
|
||||
var out struct {
|
||||
Vision bool `json:"vision"`
|
||||
Budget float64 `json:"budget"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil || w.Code != 200 {
|
||||
t.Fatalf("état : %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
return out.Vision, out.Budget
|
||||
}
|
||||
if v, b := etat(); v || b != 200 {
|
||||
t.Fatalf("sans projecteur : vision=%v budget=%v", v, b)
|
||||
}
|
||||
visionServer(t, "")
|
||||
if v, _ := etat(); !v {
|
||||
t.Fatal("projecteur vu par le moteur : vision attendue")
|
||||
}
|
||||
if w := appelOAIInterne(t, "GET", "/internal/v1/turn/keepimg", ""); w.Code != 405 {
|
||||
t.Fatalf("GET : %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Une image relayée rangée par référence, relue à l'octet près ; sinon elle
|
||||
// reste éphémère.
|
||||
func TestFacadeKeepImgRangement(t *testing.T) {
|
||||
testHome(t)
|
||||
t.Setenv(internalTokenEnv, jetonTest)
|
||||
store := func(content string) map[string]any {
|
||||
w := appelOAIInterne(t, "POST", "/internal/v1/turn/keepimg/store", `{"msg":{"role":"user","content":`+content+`}}`)
|
||||
var out map[string]any
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil || w.Code != 200 {
|
||||
t.Fatalf("rangement : %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
return out
|
||||
}
|
||||
out := store(`[{"type":"text","text":"Voici la capture demandée."},{"type":"image_url","image_url":{"url":"data:image/png;base64,iVBORw0KGgo="}}]`)
|
||||
if out["ok"] != true || out["weight"] != float64(8) {
|
||||
t.Fatalf("rangée : %v", out)
|
||||
}
|
||||
msg := out["msg"].(map[string]any)
|
||||
parts := msg["content"].([]any)
|
||||
if parts[0].(map[string]any)["text"] != "Voici la capture demandée." {
|
||||
t.Fatalf("légende : %v", parts)
|
||||
}
|
||||
ref := parts[1].(map[string]any)["image_url"].(map[string]any)["url"].(string)
|
||||
if !strings.HasPrefix(ref, imgRefScheme) {
|
||||
t.Fatalf("pas par référence : %s", ref)
|
||||
}
|
||||
if back, ok := chatImageDataURL(ref); !ok || back != "data:image/png;base64,iVBORw0KGgo=" {
|
||||
t.Fatalf("relecture : %v %.40s", ok, back)
|
||||
}
|
||||
if ents, _ := os.ReadDir(chatImgDir()); len(ents) != 1 {
|
||||
t.Fatalf("%d fichier(s) rangé(s)", len(ents))
|
||||
}
|
||||
for _, c := range []string{
|
||||
`"texte seul"`,
|
||||
`[{"type":"image_url","image_url":{"url":"https://exemple.org/a.png"}}]`,
|
||||
`[{"type":"image_url","image_url":{"url":"data:image/x-inconnu;base64,AAAA"}}]`,
|
||||
`[{"type":"image_url","image_url":{"url":"data:image/png;base64,pas du base64"}}]`,
|
||||
`[{"type":"image_url","image_url":{"url":"data:image/png;base64,iVBORw0KGgo="}},{"type":"image_url","image_url":{"url":"data:image/png;base64,iVBORw0KGgo="}}]`,
|
||||
} {
|
||||
if out := store(c); out["ok"] != false {
|
||||
t.Errorf("%s : %v", c, out)
|
||||
}
|
||||
}
|
||||
if w := appelOAIInterne(t, "POST", "/internal/v1/turn/keepimg/store", `{`); w.Code != 400 {
|
||||
t.Fatalf("corps illisible : %d", w.Code)
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,10 @@ func withInternalAPI(next http.Handler) http.Handler {
|
||||
handleInternalTurnPerf(w, r)
|
||||
case "/internal/v1/turn/slot":
|
||||
handleInternalTurnSlot(w, r)
|
||||
case "/internal/v1/turn/keepimg":
|
||||
handleInternalTurnKeepImg(w, r)
|
||||
case "/internal/v1/turn/keepimg/store":
|
||||
handleInternalTurnKeepImgStore(w, r)
|
||||
case "/internal/v1/tool/exec":
|
||||
handleInternalToolExec(w, r)
|
||||
case "/internal/v1/tool/context":
|
||||
|
||||
Reference in new issue
Block a user