mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-17 20:26: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.
155 lines
4.5 KiB
Go
155 lines
4.5 KiB
Go
package jean
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// cmdChat is the interactive terminal chat loop.
|
|
// First positional arg (if any) becomes the system prompt.
|
|
func cmdChat(args []string) error {
|
|
if !healthCheck() {
|
|
return fmt.Errorf("serveur injoignable sur :%d — jean start d'abord", LLMPort())
|
|
}
|
|
sysPrompt := strings.Join(args, " ")
|
|
msgs := []Message{}
|
|
if sysPrompt != "" {
|
|
msgs = append(msgs, Message{Role: "system", Content: sysPrompt})
|
|
}
|
|
fmt.Printf("\n%s — /reset pour vider, /sys <prompt> pour changer le system, /quit ou Ctrl-D pour sortir\n", cyan("jean chat"))
|
|
if sysPrompt != "" {
|
|
fmt.Println(dim("system: " + sysPrompt))
|
|
}
|
|
fmt.Println()
|
|
sc := bufio.NewScanner(os.Stdin)
|
|
sc.Buffer(make([]byte, 0, 64*1024), 1<<20)
|
|
for {
|
|
fmt.Print(bold("you") + " > ")
|
|
if !sc.Scan() {
|
|
fmt.Println()
|
|
return nil
|
|
}
|
|
user := sc.Text()
|
|
u := strings.TrimSpace(user)
|
|
if u == "" {
|
|
continue
|
|
}
|
|
if u == "/quit" || u == "/exit" {
|
|
return nil
|
|
}
|
|
if u == "/reset" {
|
|
kept := []Message{}
|
|
for _, m := range msgs {
|
|
if m.Role == "system" {
|
|
kept = append(kept, m)
|
|
}
|
|
}
|
|
msgs = kept
|
|
fmt.Println(dim("[contexte vidé]"))
|
|
continue
|
|
}
|
|
if strings.HasPrefix(u, "/sys ") {
|
|
newSys := strings.TrimSpace(u[5:])
|
|
msgs = msgs[:0]
|
|
if newSys != "" {
|
|
msgs = append(msgs, Message{Role: "system", Content: newSys})
|
|
}
|
|
fmt.Println(dim("[system mis à jour]"))
|
|
continue
|
|
}
|
|
msgs = append(msgs, Message{Role: "user", Content: user})
|
|
full := strings.Builder{}
|
|
inReason := false
|
|
var stats *StatsEvent
|
|
// Print the assistant prefix once; reasoning is shown inline with a tag.
|
|
fmt.Print(cyan("jean") + " > ")
|
|
caps := globalCaps()
|
|
// Compaction proactive (façon Hermes) : on résume les vieux tours quand
|
|
// l'historique dépasse le seuil, au lieu d'imposer un /reset.
|
|
if compacted, changed := MaybeCompact(context.Background(), msgs, caps, 0); changed {
|
|
msgs = compacted
|
|
fmt.Println(dim("[contexte compacté pour tenir dans la fenêtre]"))
|
|
}
|
|
extra, err := runChat(context.Background(), InjectSkills(msgs, caps), 0.7, caps, func(ev StreamEvent) bool {
|
|
switch {
|
|
case ev.Err != nil:
|
|
fmt.Printf("\n%s\n", red("[erreur] "+ev.Err.Error()))
|
|
case ev.NewHistory != nil:
|
|
// Compaction survenue pendant le tour : elle REMPLACE l'historique (elle
|
|
// contient déjà le tour en cours), préfixe système injecté retiré. Sans
|
|
// ça le terminal repartirait du fil complet au tour suivant.
|
|
base := ev.NewHistory
|
|
for len(base) > 0 && base[0].Role == "system" {
|
|
base = base[1:]
|
|
}
|
|
msgs = append([]Message(nil), base...)
|
|
fmt.Println(dim("\n[contexte compacté pour tenir dans la fenêtre]"))
|
|
case ev.Stats != nil:
|
|
stats = ev.Stats
|
|
case ev.DropReasoning:
|
|
// Le tour a « pensé sans agir » : on relance. Impossible d'effacer le
|
|
// texte déjà imprimé en terminal — on referme juste la ligne reasoning.
|
|
if inReason {
|
|
fmt.Print("\n")
|
|
inReason = false
|
|
}
|
|
case ev.ToolUsed != nil:
|
|
if ev.ToolUsed.Done || ev.ToolUsed.Typing {
|
|
break // résultat / frappe live affichés côté web ; en terminal on garde l'annonce seule
|
|
}
|
|
icon := "🧠"
|
|
verb := "mémoire"
|
|
switch ev.ToolUsed.Name {
|
|
case "bash":
|
|
icon = "⚙️"
|
|
verb = "exécution"
|
|
case "edit":
|
|
icon = "✏️"
|
|
verb = "édition"
|
|
case "web_search", "web_open", "web_read", "web_grep":
|
|
icon = "🌐"
|
|
verb = "web"
|
|
}
|
|
if inReason {
|
|
fmt.Print("\n")
|
|
inReason = false
|
|
}
|
|
fmt.Printf("\n%s %s : %s\n%s ", dim(icon+" "+verb), "", magenta(ev.ToolUsed.Label), cyan("jean")+" >")
|
|
case ev.Reasoning != "":
|
|
if !inReason {
|
|
fmt.Print(magenta("[reasoning] ") + dim(""))
|
|
inReason = true
|
|
}
|
|
fmt.Print(dim(ev.Reasoning))
|
|
case ev.Content != "":
|
|
if inReason {
|
|
fmt.Print("\n" + cyan("jean") + " > ")
|
|
inReason = false
|
|
}
|
|
full.WriteString(ev.Content)
|
|
fmt.Print(ev.Content)
|
|
}
|
|
return true
|
|
})
|
|
if inReason {
|
|
fmt.Println()
|
|
}
|
|
fmt.Println()
|
|
if stats != nil {
|
|
fmt.Printf("%s prefill %d tok · %.0f tok/s decode %d tok · %.1f tok/s\n",
|
|
dim("→"), stats.PromptTokens, stats.PromptPerSecond, stats.GenTokens, stats.GenPerSecond)
|
|
}
|
|
fmt.Println()
|
|
if err == nil {
|
|
// Persist the tool turns (skill reads, shell runs) BEFORE the final
|
|
// answer, so next turn the model remembers it already did them instead
|
|
// of re-invoking the same skill/command from scratch.
|
|
msgs = append(msgs, extra...)
|
|
msgs = append(msgs, Message{Role: "assistant", Content: full.String()})
|
|
}
|
|
}
|
|
}
|