From 8bac7c56bc60c818cad46fc5b647e15729f73e1b Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 11 Jul 2026 16:25:52 +0200 Subject: [PATCH] fix(local-llm): weiche Tool-Fehler eskalieren (Exit 0 + Fehlertext) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Escalate-on-Tool-Error-Logik griff nur bei hartem FEHLER-Prefix (Exit != 0). Action-Skills melden Fehlschlaege aber oft im stdout-Text bei Exit 0 (Spotify: "Fehler beim Uebertragen", "Geraet nicht gefunden"). Das lokale LLM las den Fehler dann brav vor statt zu eskalieren — und wiederholte beim Nachhaken dieselbe leere Absichtserklaerung. Generisch (nicht Spotify-spezifisch): fuer run_*-Skills werten weiche Fehler-Marker im Ausgabetext ebenfalls als Fehlschlag → Claude uebernimmt das mehrstufige Mitdenken. Info-Tools (web_search/memory_search) ausgenommen, damit deren Inhalt das Wort "Fehler" tragen darf. Eskalieren ist immer sicher. Co-Authored-By: Claude Opus 4.8 --- aria-brain/agent.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/aria-brain/agent.py b/aria-brain/agent.py index 7a6ee8a..01c299c 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -1150,6 +1150,32 @@ class Agent: _LOCAL_WINDOW_TURNS = 8 # nur die letzten N Turns ans lokale Modell (Speed) _LOCAL_TOOL_ITERATIONS = 3 # max Tool-Runden lokal, sonst → Claude + # Weiche Fehler-Marker im Tool-Ausgabetext. Viele Action-Skills geben einen + # menschenlesbaren Fehler auf stdout aus und beenden sich TROTZDEM mit Exit 0 + # (z.B. Spotify: "Fehler beim Uebertragen", "Geraet nicht gefunden"). Der + # harte FEHLER-Prefix (Exit != 0) faengt das nicht. Fuer run_*-Skills werten + # wir daher auch solche Marker als Fehlschlag → eskalieren an Claude, der + # mehrstufig weiterdenkt. Info-Tools (web_search/memory_search) sind bewusst + # AUSGENOMMEN: deren Inhalt darf das Wort "Fehler" tragen ohne dass der + # Tool-Call scheiterte. Eskalieren ist immer sicher (nur langsamer). + _SOFT_FAIL_MARKERS = ( + "fehler", "fehlgeschlagen", "nicht gefunden", "not found", + "konnte nicht", "keine verbindung", "nicht verfuegbar", + "exception", "traceback", + ) + + def _local_tool_failed(self, tool_name: str, tresult: str) -> bool: + """True wenn ein lokaler Tool-Call als gescheitert gilt (→ Claude).""" + s = (tresult or "").strip() + if not s: + return False + if s.startswith("FEHLER"): # harter Exit-Code-Fehler + return True + if tool_name.startswith("run_"): # Action-Skill: auch weiche Fehler + low = s.lower() + return any(mk in low for mk in self._SOFT_FAIL_MARKERS) + return False + # Kuratierte Tool-Auswahl fuers lokale Tier (B1b): web_search (local-only, # SearXNG) + memory_search/trigger_timer (aus META_TOOLS) + Spotify-Skill. # Bewusst klein (Speed + Sicherheit); alles andere → Claude. @@ -1210,7 +1236,7 @@ class Agent: logger.info("[router] lokal Tool-Call: %s(%s)", tname, ", ".join(targs.keys())) tresult = self._dispatch_tool(tname, targs) - if (tresult or "").strip().startswith("FEHLER"): + if self._local_tool_failed(tname, tresult): had_error = True messages.append({"role": "tool", "tool_call_id": tc.get("id") or "",