fix(local-llm): weiche Tool-Fehler eskalieren (Exit 0 + Fehlertext)

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 16:25:52 +02:00
co-authored by Claude Opus 4.8
parent 3b6d36f2de
commit 8bac7c56bc
+27 -1
View File
@@ -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 "",