v0.7.7 : la recherche internet ne tourne plus en rond

Le compactage du contexte detruisait ce dont l'IA avait besoin pour continuer
une recherche, ce qui la faisait boucler ou repartir de zero.

- Compactage : le resume est desormais fabrique a partir du torse ORIGINAL. On
  degraissait les resultats d'outils (page web -> marqueur) AVANT de resumer :
  le resumeur ne voyait plus aucune information trouvee, donc le resume n'en
  contenait aucune et l'IA relancait les memes recherches sans fin. Les
  resultats sont seulement raccourcis (1200 car.) pour le resumeur ; le torse
  degraisse ne sert plus que de repli si le resume echoue.
- Compactage : la demande EN COURS est preservee telle quelle. Pendant une
  boucle d'outils il n'y a aucun message user recent, elle tombait donc dans le
  torse et se dissolvait dans le resume, tandis que le TOUT PREMIER message de
  la conversation restait epingle en tete : l'IA repondait a la question du
  debut et abandonnait la recherche.
- Compactage : prompt du resumeur refait (findings, sources deja consultees,
  etat d'avancement) et plafond du resume porte de 1500 a 2200 caracteres.
- Appels repetes : l'avertissement passe EN TETE du resultat et, des la 2e
  redemande identique, le contenu n'est plus renvoye. Colle a la fin d'un
  resultat de plusieurs milliers de caracteres, il passait inapercu.
- Marqueur d'effacement du compactage : dit explicitement de ne PAS rappeler
  l'outil (l'ancien texte se lisait comme une invitation a re-telecharger).
- Outils web : sortie bornee a 8000 caracteres comme le shell. Un
  web_read(limit=500) pouvait injecter 25000 caracteres d'un coup et declencher
  les compactages en cascade.
- Bulles d'outils : la borne d'affichage passe de 4000 a 12000 caracteres, donc
  l'etiquette « ~N tok » cesse d'afficher toujours ~1004 (la valeur du plafond)
  et donne la vraie taille. Le chemin « appel identique non rejoue » envoyait
  lui le resultat brut sans troncature.
This commit is contained in:
nathaninline committed 2026-08-04 22:28:19 +02:00
1 parent aa92d2ab49
commit 9dedba0e47
10 files changed
+268 -52

No files matched your search

+62
View File
@@ -101,6 +101,68 @@ func TestCompactBoundsSplitsLongToolLoop(t *testing.T) {
}
}
// Régression : après compaction pendant une recherche web, la DEMANDE EN COURS
// doit encore figurer telle quelle dans l'historique. Sans ça, le seul message
// `user` restant était le tout premier de la conversation (épinglé en tête) et
// le modèle répondait à celui-là au lieu de continuer la recherche.
func TestCompactKeepsPendingRequest(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("première question de la conversation"), am("ok"),
um("cherche les horaires du train pour Lyon"),
}
for i := 0; i < 10; i++ {
msgs = append(msgs, atc("web_read"), page(i))
}
// summarizeTranscript échoue (pas de llama-server en test) → torse dégraissé,
// ce qui n'enlève rien à ce qu'on vérifie ici : la demande doit survivre.
out, _ := compactMessages(t.Context(), msgs, Caps{})
found := false
for _, m := range out {
if m.Role == "user" && strings.Contains(msgText(m), "horaires du train") {
found = true
}
}
if !found {
t.Fatal("la demande en cours a disparu de l'historique compacté")
}
}
// Régression : ce qu'on envoie au RÉSUMEUR doit encore contenir les résultats
// d'outils. On effaçait les résultats (dégraissage) AVANT de résumer : le
// résumeur ne voyait que des marqueurs, le résumé ne pouvait donc porter aucune
// information trouvée, et l'IA relançait la même recherche après chaque
// compactage — sans jamais s'arrêter.
func TestSummaryInputKeepsToolFindings(t *testing.T) {
fait := "le train de 14h12 part quai 3"
torso := []Message{
atc("web_read"),
tm(fait + strings.Repeat(" blabla de remplissage", 300)),
}
// Même transformation que compactMessages avant l'appel au résumeur.
forSummary := make([]Message, len(torso))
for i, m := range torso {
forSummary[i] = m
if m.Role == "tool" {
if r := []rune(msgText(m)); len(r) > compactToolSummaryLen {
forSummary[i].Content = string(r[:compactToolSummaryLen]) + "\n[…suite coupée]"
}
}
}
tr := renderTranscript(forSummary)
if !strings.Contains(tr, fait) {
t.Fatal("le fait trouvé n'atteint pas le résumeur : le résumé sera vide d'information")
}
if strings.Contains(tr, compactPrunedMarker) {
t.Fatal("le résumeur reçoit des marqueurs d'effacement au lieu du contenu")
}
if len([]rune(tr)) > 4000 {
t.Fatalf("transcription non bornée (%d runes) : le résumeur va déborder", len([]rune(tr)))
}
}
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")})