From 5ef02672d247ed0f63c8e1df1b28212c93b88cfa Mon Sep 17 00:00:00 2001 From: nathaninline Date: Mon, 6 Jul 2026 11:38:02 +0200 Subject: [PATCH] =?UTF-8?q?mode=20agent=20:=20toggle=20web=20pour=20d?= =?UTF-8?q?=C3=A9sactiver=20la=20limite=20d'appels=20d'outils=20(config=20?= =?UTF-8?q?TOOL=5FLIMIT)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La limite iter<8 (message [stop: trop d'appels d'outils]) est désormais pilotée par TOOL_LIMIT dans config.env, exposée par un interrupteur sous Mode agent. Activée par défaut ; désactivée => plafond quasi illimité (l'anti-boucle sur appels répétés reste actif). Lu à chaque tour, pas de restart nécessaire. Tunnelé tel quel via /api/e2e/req pour ajean.link. --- config.go | 21 +++++++++++++++++++++ llm.go | 7 ++++++- ui/index.html | 8 ++++++++ web.go | 21 ++++++++++++++++++++- 4 files changed, 55 insertions(+), 2 deletions(-) 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) {