mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-14 10:48:35 +02:00
Le compactage ne se declenchait qu'au DEBUT d'un tour, avec le contexte du tour precedent. Le tour qui faisait franchir le seuil se terminait donc a 81% sans rien declencher : la jauge restait haute, seul le bouton manuel apparaissait, et le compactage automatique semblait mort. - seuil reteste a trois moments au lieu d'un : debut de tour (inchange), PENDANT la boucle d'outils, et EN FIN de tour des que le seuil est franchi - une compaction faite en cours de tour etait perdue : l'appelant reconstruisait l'historique en ajoutant les messages du tour a sa copie d'AVANT compaction. Le fil complet revenait, la jauge retombait a 43% puis explosait a 92% au message suivant. Elle publie desormais sa nouvelle vue (StreamEvent.NewHistory), qui REMPLACE l'historique. Meme correction pour le filet de secours sur erreur 500 et pour le client terminal. - la queue protegee reculait jusqu'au message `user` precedent. Pendant une longue recherche il n'y en a aucun : la queue avalait toute la sequence d'outils, torse vide, compaction sans effet — les pages web qui remplissent la fenetre etaient justement celles qui etaient protegees. Elle peut maintenant demarrer sur un `assistant`, frontiere tout aussi sure (il part avec ses resultats d'outils). Test de regression ajoute. - une compaction sans effet ne disait plus rien : toast explicite + une ligne [compact] par decision dans le journal (ctx, seuil, avant/apres, changé) UI : la mini-banniere au-dessus de la saisie doublonnait les trois points animes de l'IA. L'indicateur existant est desormais etiquete « compactage du contexte… » en accent, et une marque discrete reste DANS le fil a l'endroit de la coupure — elle survit au rafraichissement, contrairement au toast.
111 lines
4.2 KiB
Go
111 lines
4.2 KiB
Go
package jean
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// helpers pour construire des historiques de test lisibles.
|
|
func um(s string) Message { return Message{Role: "user", Content: s} }
|
|
func am(s string) Message { return Message{Role: "assistant", Content: s} }
|
|
func atc(name string) Message {
|
|
return Message{Role: "assistant", ToolCalls: []ToolCall{{ID: "c1", Function: ToolCallFunc{Name: name, Arguments: "{}"}}}}
|
|
}
|
|
func tm(s string) Message { return Message{Role: "tool", ToolCallID: "c1", Content: s} }
|
|
|
|
func TestCompactBoundsProtectsHead(t *testing.T) {
|
|
msgs := []Message{
|
|
{Role: "system", Content: "sys"},
|
|
um("premier"), am("r1"),
|
|
um("q2"), am("r2"),
|
|
um("q3"), am("r3"),
|
|
}
|
|
// budget minuscule → queue = juste le dernier tour, tête = system + 1er user.
|
|
head, tail := compactBounds(msgs, 1)
|
|
if head != 2 { // system + premier user
|
|
t.Fatalf("head = %d, attendu 2", head)
|
|
}
|
|
// Frontière sûre = user ou assistant (jamais un `tool`, qui serait orphelin).
|
|
if r := msgs[tail].Role; r != "user" && r != "assistant" {
|
|
t.Fatalf("la queue doit démarrer sur un user ou un assistant, obtenu %q", r)
|
|
}
|
|
if tail <= head {
|
|
t.Fatalf("torse vide (tail=%d head=%d) alors qu'il y a du milieu à compacter", tail, head)
|
|
}
|
|
}
|
|
|
|
// La queue ne doit jamais démarrer entre un assistant+tool_calls et ses
|
|
// résultats `tool` : elle recule jusqu'à la frontière sûre précédente (user ou
|
|
// assistant), donc l'assistant part dans la queue AVEC ses résultats.
|
|
func TestCompactBoundsKeepsToolPairs(t *testing.T) {
|
|
msgs := []Message{
|
|
um("q1"), am("r1"),
|
|
um("q2"), atc("bash"), tm("sortie longue"), am("r2"),
|
|
um("q3"), am("r3"),
|
|
}
|
|
// budget moyen qui, sans le recul, couperait au milieu du tour outillé.
|
|
_, tail := compactBounds(msgs, msgTokens(msgs[6])+msgTokens(msgs[7])+msgTokens(msgs[5])+1)
|
|
if r := msgs[tail].Role; r != "user" && r != "assistant" {
|
|
t.Fatalf("la queue démarre sur %q : frontière non sûre (orphelin tool possible)", r)
|
|
}
|
|
// Vérifie qu'aucun message `tool` de la queue n'a perdu son assistant parent.
|
|
for i := tail; i < len(msgs); i++ {
|
|
if msgs[i].Role == "tool" {
|
|
if i == tail || (msgs[i-1].Role != "assistant" && msgs[i-1].Role != "tool") {
|
|
t.Fatalf("message tool orphelin à l'index %d de la queue", i)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Régression : DEUXIÈME compaction à l'intérieur d'un même tour. Une longue
|
|
// boucle d'outils (recherche web : dix pages lues d'affilée) ne contient AUCUN
|
|
// message `user` — reculer jusqu'à un `user` faisait donc avaler toute la
|
|
// séquence par la queue, torse vide, compaction sans effet. La queue doit
|
|
// pouvoir démarrer sur un `assistant`, et les gros résultats d'outils du début
|
|
// doivent se retrouver dans le torse (donc résumés).
|
|
func TestCompactBoundsSplitsLongToolLoop(t *testing.T) {
|
|
page := func(n int) Message {
|
|
return tm("contenu de page web très long " + string(rune('a'+n)) + strings.Repeat("x", 400))
|
|
}
|
|
msgs := []Message{
|
|
um("premier"), am("ok"), // head : 1er user protégé
|
|
um("cherche des trucs"), // la dernière demande utilisateur du tour
|
|
}
|
|
for i := 0; i < 10; i++ {
|
|
msgs = append(msgs, atc("web_read"), page(i))
|
|
}
|
|
head, tail := compactBounds(msgs, estimateTokens(msgs)/4)
|
|
if tail <= head {
|
|
t.Fatalf("torse vide (tail=%d head=%d) : la boucle d'outils n'est pas compactable", tail, head)
|
|
}
|
|
if r := msgs[tail].Role; r != "user" && r != "assistant" {
|
|
t.Fatalf("la queue démarre sur %q : frontière non sûre", r)
|
|
}
|
|
// Aucun `tool` orphelin en queue.
|
|
for i := tail; i < len(msgs); i++ {
|
|
if msgs[i].Role == "tool" && (i == tail || (msgs[i-1].Role != "assistant" && msgs[i-1].Role != "tool")) {
|
|
t.Fatalf("message tool orphelin à l'index %d de la queue", i)
|
|
}
|
|
}
|
|
// Le torse doit bien contenir des résultats d'outils (c'est ce qui remplit
|
|
// la fenêtre) — sinon la compaction ne libérerait rien.
|
|
tools := 0
|
|
for _, m := range msgs[head:tail] {
|
|
if m.Role == "tool" {
|
|
tools++
|
|
}
|
|
}
|
|
if tools == 0 {
|
|
t.Fatal("aucun résultat d'outil dans le torse : rien à gagner à compacter")
|
|
}
|
|
}
|
|
|
|
func TestEstimateTokensGrows(t *testing.T) {
|
|
small := estimateTokens([]Message{um("court")})
|
|
big := estimateTokens([]Message{um("un message nettement plus long que le précédent pour dépasser")})
|
|
if big <= small {
|
|
t.Fatalf("estimateTokens ne croît pas avec la taille: small=%d big=%d", small, big)
|
|
}
|
|
}
|