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. -->
+
+