mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-18 12:46:56 +02:00
Socle du renommage jean -> AJEAN, sans aucun impact visible pour le parc installe. Trois briques : - module github.com/nathaninline/ajean, cmd/ajean, internal/ajean, package ajean. Purement interne. - AjeanHome() remplace JeanHome(). Les defauts deviennent %ProgramData%\ajean et /etc/ajean, avec migration du dossier existant au premier lancement. La migration est un os.Rename dans le dossier parent de l'ancien chemin : intra-volume, donc atomique et instantane meme avec des .gguf de plusieurs dizaines de Go. Si le rename echoue (handle ouvert sous Windows, droits, volume en lecture seule) on CONTINUE sur l'ancien chemin et on retentera : jamais de copie, jamais de suppression, le pire cas est "rien n'a bouge". $AJEAN_HOME et $JEAN_HOME restent tous deux honores, ainsi que /etc/default/ajean et /etc/default/jean : un chemin impose a la main est un ordre, on ne le migre pas. - l'auto-MAJ accepte desormais ajean-<os>-<arch> ET jean-<os>-<arch>, dans cet ordre. Indispensable dans les deux sens : les binaires deja installes ne connaissent que jean-*, et ce binaire-ci doit pouvoir s'installer depuis une release anterieure au renommage. Couvert par des tests, dont le repli quand un handle ouvert bloque le rename sous Windows.
158 lines
4.6 KiB
Go
158 lines
4.6 KiB
Go
package ajean
|
|
|
|
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 "write":
|
|
icon = "📄"
|
|
verb = "écriture"
|
|
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()})
|
|
}
|
|
}
|
|
}
|