mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 11:49:43 +02:00
API interne : suites d'un outil et archive recall pour le tour tenu par Rust
/internal/v1/tool/exec rend aussi ce que la boucle Go tire du résultat après l'exécution, et qui demande le disque ou le navigateur (toolStepExtras) : capture de web_screenshot pour la bulle, aperçu du navigateur après une action qui change la page (jamais après une erreur), message qui relaie l'image au modèle (capture avec la vision, see_image). /internal/v1/recall/archive range un bloc verbatim pour l'outil recall : son numéro vient de la séquence bbolt, seul ce process peut le donner. 423 si la mémoire est verrouillée, rien d'écrit en clair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
5 files changed
+140
-2
No files matched your search
@@ -83,6 +83,8 @@ func withInternalAPI(next http.Handler) http.Handler {
|
||||
handleInternalLeaseRelease(w, r)
|
||||
case "/internal/v1/engine/stop":
|
||||
handleInternalEngineStop(w, r)
|
||||
case "/internal/v1/recall/archive":
|
||||
handleInternalRecallArchive(w, r)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// API interne de l'archive recall (lot 6 de la migration Rust).
|
||||
//
|
||||
// La compaction range VERBATIM les gros blocs du torse qu'elle résume, pour
|
||||
// que l'outil recall puisse les rendre plus tard. Le numéro d'un bloc vient de
|
||||
// la séquence bbolt du bucket : seul ce process peut le donner. Une
|
||||
// compaction faite par la façade fait donc archiver ici.
|
||||
//
|
||||
// POST /internal/v1/recall/archive {"label":…,"role":…,"content":…}
|
||||
// → 200 {"id":"r7"} · 423 mémoire verrouillée (rien d'écrit en clair)
|
||||
|
||||
const internalRecallMaxBody = 32 << 20
|
||||
|
||||
func handleInternalRecallArchive(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Allow", http.MethodPost)
|
||||
sendJSON(w, http.StatusMethodNotAllowed, map[string]any{"error": "POST attendu"})
|
||||
return
|
||||
}
|
||||
var in struct {
|
||||
Label string `json:"label"`
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
raw, err := io.ReadAll(io.LimitReader(r.Body, internalRecallMaxBody+1))
|
||||
if err != nil || len(raw) > internalRecallMaxBody || json.Unmarshal(raw, &in) != nil || in.Content == "" {
|
||||
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "bloc illisible"})
|
||||
return
|
||||
}
|
||||
id, err := archiveRecallBlock(in.Label, in.Role, in.Content)
|
||||
if errors.Is(err, errMemLocked) {
|
||||
sendJSON(w, http.StatusLocked, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
sendJSON(w, http.StatusServiceUnavailable, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
sendJSON(w, http.StatusOK, map[string]any{"id": id})
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Un bloc archivé par la façade se relit par l'outil recall, comme s'il avait
|
||||
// été archivé par la compaction du Go.
|
||||
func TestRecallInterneArchive(t *testing.T) {
|
||||
testHome(t)
|
||||
t.Setenv(internalTokenEnv, jetonTest)
|
||||
w := appelOAIInterne(t, "POST", "/internal/v1/recall/archive", `{"label":"bash ls","role":"tool","content":"a.go\nb.go"}`)
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("archive : %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
id, _ := leaseJSON(t, w.Body.String())["id"].(string)
|
||||
if !strings.HasPrefix(id, "r") {
|
||||
t.Fatalf("id : %q", id)
|
||||
}
|
||||
if got := toolRecall(map[string]any{"id": id}); !strings.Contains(got, "a.go\nb.go") || !strings.Contains(got, "bash ls") {
|
||||
t.Fatalf("recall : %q", got)
|
||||
}
|
||||
for _, body := range []string{"", "{", `{"label":"x"}`} {
|
||||
if w := appelOAIInterne(t, "POST", "/internal/v1/recall/archive", body); w.Code != 400 {
|
||||
t.Fatalf("%q : %d", body, w.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// API interne d'exécution des outils (lot 6 de la migration Rust).
|
||||
@@ -16,7 +17,14 @@ import (
|
||||
//
|
||||
// POST /internal/v1/tool/exec
|
||||
// {"name":"bash","args":{"command":"ls"},"caps":{"Agent":true,"Code":false,…}}
|
||||
// → {"label":"ls","result":"…","diff":[…],"added":N,"removed":N,"vision_img":{…},"ask":{…}}
|
||||
// → {"label":"ls","result":"…","diff":[…],"added":N,"removed":N,"vision_img":{…},"ask":{…},
|
||||
// "image":"…","shot":"…","relay":{…}}
|
||||
//
|
||||
// image, shot et relay sont ce que la boucle Go tire du résultat APRÈS
|
||||
// l'exécution, et qui demande le disque ou le navigateur (toolStepExtras) :
|
||||
// la capture à montrer dans la bulle, l'aperçu en direct du navigateur piloté,
|
||||
// et le message `user` qui porte l'image au modèle (capture, see_image) —
|
||||
// éphémère, à ne jamais ranger dans l'historique.
|
||||
//
|
||||
// Le contexte de la requête porte l'arrêt : si l'appelant coupe la connexion
|
||||
// (bouton stop), la commande en cours est interrompue comme dans le Go.
|
||||
@@ -46,8 +54,35 @@ func handleInternalToolExec(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
label := toolCallLabel(in.Name, in.Args)
|
||||
res := execTool(r.Context(), in.Name, in.Args, label, in.Caps)
|
||||
image, shot, relay := toolStepExtras(in.Name, label, res)
|
||||
sendJSON(w, http.StatusOK, struct {
|
||||
Label string `json:"label"`
|
||||
toolExecResult
|
||||
}{label, res})
|
||||
Image string `json:"image,omitempty"`
|
||||
Shot string `json:"shot,omitempty"`
|
||||
Relay *Message `json:"relay,omitempty"`
|
||||
}{label, res, image, shot, relay})
|
||||
}
|
||||
|
||||
// toolStepExtras : ce que runChatLoop tire d'un résultat d'outil après
|
||||
// l'exécution — capture de web_screenshot pour la bulle, aperçu du navigateur
|
||||
// après une action qui change la page (jamais après une erreur), image relayée
|
||||
// au modèle (capture avec la vision active, ou see_image réussi).
|
||||
func toolStepExtras(name, label string, res toolExecResult) (image, shot string, relay *Message) {
|
||||
if !strings.HasPrefix(res.Result, "[erreur]") && cuAutoShotTool(name) {
|
||||
shot = cuAutoShot()
|
||||
}
|
||||
if name == "web_screenshot" {
|
||||
image = capturedRelPath(res.Result)
|
||||
if image != "" {
|
||||
if m, ok := screenshotImageMessage(image); ok {
|
||||
relay = &m
|
||||
}
|
||||
}
|
||||
}
|
||||
if res.VisionImg != nil {
|
||||
m := seeImageMessage(label, res.VisionImg)
|
||||
relay = &m
|
||||
}
|
||||
return image, shot, relay
|
||||
}
|
||||
@@ -60,3 +60,27 @@ func TestOutilInterneAppelIllisible(t *testing.T) {
|
||||
t.Fatalf("GET : %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Ce que la boucle tire du résultat après l'exécution : capture de la bulle,
|
||||
// image relayée au modèle — rien après une erreur.
|
||||
func TestOutilInterneSuitesDuResultat(t *testing.T) {
|
||||
testHome(t)
|
||||
image, shot, relay := toolStepExtras("web_screenshot", "", toolExecResult{
|
||||
Result: "Capture : ",
|
||||
})
|
||||
if image != "captures/a.png" || shot != "" || relay != nil {
|
||||
t.Fatalf("capture : %q %q %v (vision coupée : pas de relais)", image, shot, relay)
|
||||
}
|
||||
img := map[string]any{"type": "image_url", "image_url": map[string]any{"url": "data:image/png;base64,AA=="}}
|
||||
_, _, relay = toolStepExtras("see_image", "photo.png", toolExecResult{Result: "ok", VisionImg: img})
|
||||
if relay == nil || relay.Role != "user" {
|
||||
t.Fatalf("see_image : %v", relay)
|
||||
}
|
||||
b, _ := json.Marshal(relay)
|
||||
if !strings.Contains(string(b), "Image demandée (photo.png)") || !strings.Contains(string(b), "data:image/png") {
|
||||
t.Fatalf("relais : %s", b)
|
||||
}
|
||||
if _, shot, _ := toolStepExtras("browser_click", "x", toolExecResult{Result: "[erreur] rien"}); shot != "" {
|
||||
t.Fatalf("aperçu après une erreur : %q", shot)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user