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) {