diff --git a/config.go b/config.go
index bb614aa..fe6ec9e 100644
--- a/config.go
+++ b/config.go
@@ -139,6 +139,27 @@ 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
+}
+
// LLMPort returns the configured server port (config.env PORT), default 8080.
func LLMPort() int {
if p, ok := ReadConfig()["PORT"]; ok {
diff --git a/llm.go b/llm.go
index 6ea8b35..00efb07 100644
--- a/llm.go
+++ b/llm.go
@@ -356,7 +356,12 @@ 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
- for iter := 0; iter < 8; iter++ {
+ // 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++ {
payload := map[string]any{
"model": "jean",
"messages": messages,
diff --git a/ui/index.html b/ui/index.html
index 25fa8a7..03ca6a6 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -186,6 +186,8 @@ button:disabled{opacity:.5;cursor:not-allowed}
Mode agent
⚠️ active l'accès machine (shell complet via bash sur le serveur) et la mémoire — l'IA peut inspecter, lancer des scripts et lire/écrire ses pages mémoire (timeout 30s, max 300s)
+
+
activé par défaut : coupe l'IA après ~8 appels d'outils (message [stop: trop d'appels d'outils]). Désactive-le si tu veux la laisser enchaîner sans plafond (l'anti-boucle reste actif).
Mémoire
@@ -414,6 +416,7 @@ async function loadAgent(){
const s=await jget('/api/agent');
const on = s.enabled;
document.getElementById('agent-toggle').checked = on;
+ document.getElementById('toollimit-toggle').checked = (s.tool_limit !== false);
document.getElementById('agent-badge').innerHTML = on
? 'on' : 'off';
document.getElementById('brand').classList.toggle('agent', on);
@@ -430,6 +433,11 @@ async function toggleAgent(){
await jpost('/api/agent/toggle',{on});
loadAgent();
}
+async function toggleToolLimit(){
+ const on=document.getElementById('toollimit-toggle').checked;
+ await jpost('/api/agent/tool-limit',{on});
+ loadAgent();
+}
async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadCfg(),loadPresets(),loadAgent()]); }
async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); }
function openBenchModal(){ document.getElementById('bench-modal').style.display = 'flex'; }
diff --git a/web.go b/web.go
index 4e4d16e..f60e3bd 100644
--- a/web.go
+++ b/web.go
@@ -85,6 +85,7 @@ func newWebMux() *http.ServeMux {
api("/api/preset/delete", handlePresetDelete)
api("/api/agent", handleAgent)
api("/api/agent/toggle", handleAgentToggle)
+ api("/api/agent/tool-limit", handleToolLimitToggle)
// Alias rétro-compat : l'ancien portail ajean.link (dépôt jean-relay) pilote
// encore l'agent via /api/tools* et /api/skills/toggle à travers le tunnel E2E.
// On les mappe sur le mode agent unifié le temps que le portail soit mis à jour.
@@ -436,7 +437,25 @@ func handleAgent(w http.ResponseWriter, r *http.Request) {
for _, p := range pages {
out = append(out, map[string]any{"name": p.Name, "desc": p.Title})
}
- sendJSON(w, 200, map[string]any{"enabled": agentEnabled(), "pages": out, "skills": out})
+ sendJSON(w, 200, map[string]any{"enabled": agentEnabled(), "tool_limit": toolLimitEnabled(), "pages": out, "skills": out})
+}
+
+// handleToolLimitToggle active/désactive le plafond d'appels d'outils par tour
+// (config.env TOOL_LIMIT). On=limité (défaut), off=quasi illimité.
+func handleToolLimitToggle(w http.ResponseWriter, r *http.Request) {
+ var req struct {
+ On bool `json:"on"`
+ }
+ _ = json.NewDecoder(r.Body).Decode(&req)
+ val := ""
+ if !req.On {
+ val = "off"
+ }
+ if err := SetConfigKey("TOOL_LIMIT", val); err != nil {
+ sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
+ return
+ }
+ sendJSON(w, 200, map[string]any{"ok": true, "tool_limit": toolLimitEnabled()})
}
func handleAgentToggle(w http.ResponseWriter, r *http.Request) {