diff --git a/README.md b/README.md index e2c89b1..460ecdd 100644 --- a/README.md +++ b/README.md @@ -178,8 +178,7 @@ Tout vit sous **`$JEAN_HOME`** (défaut `/etc/jean` sur Linux/macOS, `%ProgramDa | `CUDA_VISIBLE_DEVICES` | GPU à utiliser (réglé par `jean gpu`) | tous | | `KV_TYPE` (`_K`/`_V`) | quantization du cache KV | — | | `REASONING` | passthrough du mode raisonnement | — | -| `REASONING_BUDGET` | plafond de tokens de réflexion passé à llama-server ; `-1` = illimité (l'anti-boucle est géré côté agent) | `-1` | -| `TOOL_LIMIT` | plafond d'appels d'outils par tour en mode agent ; `off` = quasi illimité (réglable aussi depuis l'UI) | activé | +| `REASONING_BUDGET` | plafond de tokens de réflexion passé à llama-server ; `-1` = illimité | `-1` | | `CRAWL4AI_URL` | URL du serveur Crawl4AI pour l'accès internet (réglé par `jean internet url`) | — | | `MEM_MODE` | mode mémoire de l'IA : `off` / `ondemand` / `always` (réglé par `jean memory`) | `always` | | `EXTRA_ARGS` | ajouté tel quel à `llama-server` | — | diff --git a/cmd/jean/resource_windows_amd64.syso b/cmd/jean/resource_windows_amd64.syso index c889c31..5c5d047 100644 Binary files a/cmd/jean/resource_windows_amd64.syso and b/cmd/jean/resource_windows_amd64.syso differ diff --git a/cmd/jean/resource_windows_arm64.syso b/cmd/jean/resource_windows_arm64.syso index edacfa8..604bb92 100644 Binary files a/cmd/jean/resource_windows_arm64.syso and b/cmd/jean/resource_windows_arm64.syso differ diff --git a/cmd/jean/versioninfo.json b/cmd/jean/versioninfo.json index 24a58f0..999a700 100644 --- a/cmd/jean/versioninfo.json +++ b/cmd/jean/versioninfo.json @@ -3,13 +3,13 @@ "FileVersion": { "Major": 0, "Minor": 6, - "Patch": 2, + "Patch": 3, "Build": 0 }, "ProductVersion": { "Major": 0, "Minor": 6, - "Patch": 2, + "Patch": 3, "Build": 0 }, "FileFlagsMask": "3f", @@ -25,7 +25,7 @@ "LegalCopyright": "Copyright (c) 2026 AJEAN contributors. MIT License.", "OriginalFilename": "jean.exe", "ProductName": "AJEAN", - "ProductVersion": "0.6.2", + "ProductVersion": "0.6.3", "Comments": "https://github.com/nathaninline/jean — projet open source (MIT)" }, "VarFileInfo": { diff --git a/internal/jean/backend_config.go b/internal/jean/backend_config.go index 76357c1..75b6f7a 100644 --- a/internal/jean/backend_config.go +++ b/internal/jean/backend_config.go @@ -164,26 +164,9 @@ func SetConfigKey(key, value string) error { return os.WriteFile(confPath(), []byte(content), 0o644) } -// toolLimitEnabled reports whether the per-turn tool-call cap is active. -// Default: true (limité). Only an explicit "off"/"false"/"0"/"no" in config.env -// (TOOL_LIMIT) disables it — anything else keeps the safety cap on. -func toolLimitEnabled() bool { - switch strings.ToLower(strings.TrimSpace(ReadConfig()["TOOL_LIMIT"])) { - case "off", "false", "0", "no", "non": - return false - } - return true -} - -// toolCallLimit returns the max number of agentic tool-call iterations per turn. -// 8 when the limit is on (default), a very high ceiling when the user disabled it -// from the web UI — the repeated-call anti-loop still guards against runaways. -func toolCallLimit() int { - if toolLimitEnabled() { - return 8 - } - return 1000 -} +// Le plafond d'appels d'outils par tour (TOOL_LIMIT) et l'anti-boucle ont été +// retirés : ils coupaient surtout des tours légitimes. Le bouton stop est le +// seul frein. // LLMPort returns the configured server port (config.env PORT), default 8080. func LLMPort() int { diff --git a/internal/jean/chat_tools.go b/internal/jean/chat_tools.go index 3554097..dbd72b9 100644 --- a/internal/jean/chat_tools.go +++ b/internal/jean/chat_tools.go @@ -39,7 +39,9 @@ func baseSystemPrompt(caps Caps) string { // sur-raisonner les modèles à reasoning (Qwen3) : ils émettent leur // puis le token de fin SANS appeler d'outil (~25-45 % de tours « morts » // mesurés). Une version courte et directe ramène ça à 0 %. NE PAS regonfler. - b.WriteString("You are jean, an expert assistant operating directly on this machine with real tools.") + // « Jean » avec une majuscule : c'est un nom propre, et le modèle recopie + // littéralement la casse d'ici quand il se présente (« je suis jean »). + b.WriteString("You are Jean, an expert assistant operating directly on this machine with real tools.") if caps.Mem == MemAlways { b.WriteString(" You evolve with every conversation: you actively maintain a persistent memory so nothing useful is lost between sessions.") } diff --git a/internal/jean/llm_client.go b/internal/jean/llm_client.go index b219ccf..9405871 100644 --- a/internal/jean/llm_client.go +++ b/internal/jean/llm_client.go @@ -414,10 +414,6 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps // dépassement de la fenêtre de contexte après de gros résultats d'outils), on // compacte l'historique en vol et on rejoue le tour — une seule fois. compactedRetry := false - // Anti-loop net: if the model re-emits the exact same tool call (name+args) - // several times, it's stuck — break instead of spinning to the iteration cap. - lastSig := "" - repeatSig := 0 // Appels d'outil déjà exécutés (clé = nom + arguments bruts) : sert à ne pas // rejouer deux fois exactement la même écriture dans un même échange. doneCalls := map[string]string{} @@ -426,12 +422,11 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps // (ni réponse, ni tool_call). On relance alors UNE fois le tour avec un nudge // explicite au lieu d'afficher « pas de réponse ». nudged := false - // Plafond d'itérations d'appels d'outils. Activé par défaut (8) pour éviter - // qu'un modèle parti en vrille n'enchaîne les appels indéfiniment ; l'anti- - // boucle (lastSig) protège toujours même quand la limite est désactivée via - // l'UI (TOOL_LIMIT=off → plafond très haut, quasi illimité). - maxIter := toolCallLimit() - for iter := 0; iter < maxIter; iter++ { + // Pas de plafond d'itérations ni d'anti-boucle : ils coupaient des tours + // parfaitement légitimes (une recherche enchaîne facilement des dizaines + // d'appels, parfois identiques). Le seul frein est le bouton stop, qui annule + // le contexte — c'est un choix assumé. + for iter := 0; ; iter++ { payload := map[string]any{ "model": "jean", "messages": messages, @@ -557,6 +552,20 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps finishReason = ch.FinishReason } if len(ch.Delta.ToolCalls) > 0 { + // Un appel d'outil clôt le texte : on vide MAINTENANT le reliquat + // retenu par la garde « » (voir plus bas). Sinon il n'était + // émis qu'en fin de flux, donc APRÈS l'événement d'outil, et l'UI + // (qui coupe la bulle en cours à chaque tool_used) affichait la fin + // de la phrase — souvent coupée en plein mot — dans une bulle + // séparée sous l'outil. + if thinkOpen && thinkTail.Len() > 0 { + tail := thinkTail.String() + thinkTail.Reset() + if !cb(StreamEvent{Content: tail}) { + aborted = true + break + } + } for i, tc := range ch.Delta.ToolCalls { // llama.cpp's stream may omit index; fall back to slot i. idx := i @@ -705,25 +714,6 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps } messages = append(messages, assistant) extra = append(extra, assistant) - // Loop guard: same exact call(s) as last turn? Count it; on the 3rd - // identical turn, stop so we don't churn the same command forever. - sig := strings.Builder{} - for _, tc := range tcs { - sig.WriteString(tc.Function.Name) - sig.WriteByte('\x00') - sig.WriteString(tc.Function.Arguments) - sig.WriteByte('\n') - } - if s := sig.String(); s == lastSig { - repeatSig++ - if repeatSig >= 2 { - cb(StreamEvent{Content: "\n\n[stop: appel d'outil répété en boucle — " + tcs[0].Function.Name + "]"}) - return extra, nil - } - } else { - lastSig = s - repeatSig = 0 - } // 2. Execute each tool locally and append a "tool" reply. for _, tc := range tcs { var args map[string]any @@ -887,8 +877,6 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps } return extra, nil } - cb(StreamEvent{Content: "\n\n[stop: trop d'appels d'outils]"}) - return extra, nil } // healthCheck pings llama.cpp's /health endpoint. diff --git a/internal/jean/run.go b/internal/jean/run.go index 422d3dc..5389fbc 100644 --- a/internal/jean/run.go +++ b/internal/jean/run.go @@ -10,7 +10,7 @@ import ( "strings" ) -const Version = "0.6.2" +const Version = "0.6.3" // Main est le vrai main() du binaire (cmd/jean ne fait que l'appeler). func Main() { diff --git a/internal/jean/ui/index.html b/internal/jean/ui/index.html index a301a92..d2ce372 100644 --- a/internal/jean/ui/index.html +++ b/internal/jean/ui/index.html @@ -14,6 +14,13 @@ (iOS n'accepte pas le SVG pour l'icône d'accueil). Aucun fichier externe requis. --> + +