feat(local-llm): Skill-Ausführung via local = kein TTS (speak=False)

Wenn das lokale LLM einen echten Skill (run_*, z.B. Spotify) ausführt, ist es
ein Steuerbefehl — genau wie Fast-Path. Jetzt speak=False: kein Vorlesen, und
die App beendet direkt (STOP) statt ins 30s-Gespräch zu gehen. Deckt den Fall
ab, wo Whisper sich verhört ("Nächster Slick") und local statt Fast-Path
den Skill auffängt.

Info-Tools (web_search/memory_search/trigger_timer) zählen NICHT — deren
Antwort/Bestätigung bleibt gesprochen. Reiner Brain-Fix: die App liest speak
bereits (2b48e5c), kein neuer APK nötig.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 21:33:17 +02:00
co-authored by Claude Opus 4.8
parent 245dfc4d73
commit a9e1a46a2f
+14 -1
View File
@@ -1226,6 +1226,11 @@ class Agent:
cfg = router_mod.load_config() cfg = router_mod.load_config()
if not router_mod.should_try_local(user_message, cfg): if not router_mod.should_try_local(user_message, cfg):
return None return None
# Merker: hat dieser lokale Turn einen echten Skill (run_*) ausgefuehrt?
# Dann ist es ein Steuerbefehl (z.B. Spotify) → NICHT vorlesen (speak=
# False), wie beim Fast-Path. Info-Tools (web_search/memory_search/
# trigger_timer) zaehlen NICHT — deren Antwort/Bestaetigung wird gesprochen.
self._local_turn_executed_skill = False
local_only = bool(cfg.get("localOnly")) local_only = bool(cfg.get("localOnly"))
local_model = cfg.get("localLlmModel") or "qwen3-8b" # B0.5: llama-swap-Key local_model = cfg.get("localLlmModel") or "qwen3-8b" # B0.5: llama-swap-Key
tools = self._build_local_tools() # B1b: kuratierte Tools tools = self._build_local_tools() # B1b: kuratierte Tools
@@ -1273,6 +1278,10 @@ class Agent:
logger.info("[router] lokal Tool-Call: %s(%s)", tname, logger.info("[router] lokal Tool-Call: %s(%s)", tname,
", ".join(targs.keys())) ", ".join(targs.keys()))
tresult = self._dispatch_tool(tname, targs) tresult = self._dispatch_tool(tname, targs)
# Echter Skill (run_*) ausgefuehrt = Steuerbefehl → nachher
# nicht vorlesen. Nur bei Erfolg (Fehler → eskaliert eh).
if tname.startswith("run_") and not self._local_tool_failed(tname, tresult):
self._local_turn_executed_skill = True
if self._local_tool_failed(tname, tresult): if self._local_tool_failed(tname, tresult):
had_error = True had_error = True
messages.append({"role": "tool", messages.append({"role": "tool",
@@ -1383,7 +1392,11 @@ class Agent:
# teure Claude-Aufbau + Tool-Loop wird uebersprungen. Sonst None → Claude. # teure Claude-Aufbau + Tool-Loop wird uebersprungen. Sonst None → Claude.
local_reply = self._try_local_fast_lane(user_message, active_project_id) local_reply = self._try_local_fast_lane(user_message, active_project_id)
if local_reply is not None: if local_reply is not None:
return local_reply, "local", True # ARIA-Antwort → vorlesen ok # Hat local einen echten Skill (run_*) ausgefuehrt → Steuerbefehl,
# NICHT vorlesen (kein TTS, App beendet direkt statt 30s-Gespraech) —
# genau wie Fast-Path. Sonst normale gesprochene Antwort.
speak = not getattr(self, "_local_turn_executed_skill", False)
return local_reply, "local", speak
# 2. Hot Memory (alle pinned Punkte) # 2. Hot Memory (alle pinned Punkte)
hot = self.store.list_pinned() hot = self.store.list_pinned()