diff --git a/README.md b/README.md index 0361ef0..d2fc624 100644 --- a/README.md +++ b/README.md @@ -402,7 +402,7 @@ jedem Chat-Turn im Hot-Memory-Block auf: - **oauth-reauth-reflex** — bei 401: ZUERST `oauth_get_token` (Auto-Refresh), nur bei dessen Fehler `oauth_authorize` - **no-skill-drift** — kein Drift vom Skill zu Ad-hoc-Bash-Befehlen. Skill kaputt? `skill_logs` + `skill_update`. Niemals nur SAGEN „ich baue dir einen Skill", wenn `skill_create` nicht wirklich gefeuert wird - **runtime-topology** (architektur) — ARIA laeuft als `claude`-CLI-Subprocess IM aria-proxy Container (alpine — kein python3/jq), NICHT im aria-brain. `/data/skills/` und `BRAIN_INTERNAL_URL` existieren dort nicht. Brain-Resources via Brain-Tools (`oauth_get_token`, `memory_search`, `run_` …), nicht via Bash. SSH zur VM-Host via `ssh aria@host` (Key liegt im Proxy) -- **scaffold-reflex** — Brain trackt cross-session welche externen Hosts via Bash-curl wiederholt (≥3× in 24h) ohne passenden Skill aufgerufen wurden. Ergebnis landet als `## API-Heuristik`-Block im System-Prompt. **Auto-Scaffold**: bei bekannten Hosts (Spotify, GitHub, OpenAI etc.) legt Brain den Skill automatisch an — ARIA findet ihn beim nächsten Turn vor (author=`aria-auto`) und nutzt `run_` statt curlen. Toggle via ENV `BRAIN_AUTO_SCAFFOLD=false`. Data-Source: `agent_stream.jsonl`, Cache 5 min +- **scaffold-reflex** — Brain trackt cross-session welche externen Hosts via Bash-curl wiederholt (≥3× in 24h) ohne passenden Skill aufgerufen wurden. Ergebnis landet als `## API-Heuristik`-Block im System-Prompt. **Auto-Scaffold**: bei bekannten Hosts (Spotify, GitHub, OpenAI etc.) legt Brain den Skill automatisch an — ARIA findet ihn beim nächsten Turn vor (author=`aria-auto`) und nutzt `run_` statt curlen. Toggle via ENV `BRAIN_AUTO_SCAFFOLD=false`. **Bypass-Lehre**: wenn ARIA trotz vorhandenem Skill weiter curlt (Skill-Bypass), erkennt Brain das im agent_stream und (1) injiziert einen drastischen `🚨 SKILL-BYPASS`-Hint im aktuellen System-Prompt und (2) speichert ein pinned `type=rule, source=auto-feedback` Memory mit Skill+Host (idempotent via migration_key `auto/skill-bypass/`) — damit lernt sie es cross-session, nicht nur in der aktuellen Konversation. Data-Source: `agent_stream.jsonl`, Cache 5 min - **external-api-auth-strategy** — OAuth2 → `oauth_get_token`, sonst `config_schema`, NIEMALS hardcoden ### Skill-Scaffold (Templates) diff --git a/aria-brain/agent.py b/aria-brain/agent.py index 198a809..a0860c5 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -829,6 +829,65 @@ class Agent: self._pending_events = [] return events + def _upsert_bypass_lesson(self, ev: dict) -> None: + """Speichert die Lehre aus einem Skill-Bypass als pinned Memory. + Idempotent ueber migration_key — bei Wiederholung wird der vorhandene + Punkt aktualisiert (Counter hoeher). So lernt ARIA cross-session, + nicht nur in der aktuellen Konversation.""" + from datetime import datetime, timezone + import uuid as _uuid + from memory.vector_store import COLLECTION + from qdrant_client.http import models as _qm + + skill_name = ev["skill_name"] + host = ev["host"] + count = ev["count"] + migration_key = f"auto/skill-bypass/{skill_name}" + title = f"Skill '{skill_name}' nutzen, nicht curl" + run_tool = f"run_{skill_name.replace('-', '_')}" + content = ( + f"WICHTIG fuer Performance + Stefans Wartezeit: " + f"Skill '{skill_name}' existiert und deckt {host} ab. " + f"Nutze `{run_tool}(...)` als Brain-Tool, NICHT Bash-curl gegen {host}. " + f"Brain hat {count}× erkannt dass dieser Skill umgangen wurde " + f"(letzter Vorfall: heute). Ein Skill-Aufruf = 1 Tool-Call (~3s) " + f"vs. Bash-Wrapper = 3-5 Tool-Calls (~13-20s)." + ) + + # Alte Version mit gleicher migration_key entfernen (Counter-Update) + try: + self.store.client.delete( + collection_name=COLLECTION, + points_selector=_qm.FilterSelector(filter=_qm.Filter(must=[ + _qm.FieldCondition(key="migration_key", + match=_qm.MatchValue(value=migration_key)) + ])), + ) + except Exception: + pass + + vec = self.embedder.embed(content) + now = datetime.now(timezone.utc).isoformat() + payload = { + "type": "rule", + "title": title, + "content": content, + "pinned": True, + "category": "skills", + "source": "auto-feedback", + "tags": [], + "created_at": now, + "updated_at": now, + "migration_key": migration_key, + "attachments": [], + } + self.store.client.upsert( + collection_name=COLLECTION, + points=[_qm.PointStruct(id=str(_uuid.uuid4()), vector=vec, payload=payload)], + ) + logger.info("bypass-lesson upserted: skill=%s host=%s count=%d", + skill_name, host, count) + # ── Hauptpfad: ein User-Turn → Tool-Loop → finaler Reply ── MAX_TOOL_ITERATIONS = 8 # Schutz vor Endlos-Loops @@ -936,6 +995,27 @@ class Agent: hints = _ah.compute_hints(existing_skills=all_skills, force=True) api_heuristic_section = _ah.build_section(hints) + + # BYPASS-DETECTION (Variante 3 / Lerneffekt): + # Hat ARIA in den letzten ~10min Bash-curl gegen einen Host + # gemacht OBWOHL der Skill existiert? → drastischer Hint im + # Prompt JETZT + pinned Memory speichern, damit's beim + # naechsten Turn / naechster Session weiter sichtbar ist + # ("echtes Lernen via Brain-Memory"). + bypass_events = _ah.detect_recent_bypass(all_skills, since_sec=600) + if bypass_events: + bypass_section = _ah.build_bypass_section(bypass_events) + if bypass_section: + api_heuristic_section = ( + (bypass_section + "\n\n" + api_heuristic_section) + if api_heuristic_section else bypass_section + ) + # Pinned-Memory pro Skill speichern, idempotent ueber migration_key + for ev in bypass_events: + try: + self._upsert_bypass_lesson(ev) + except Exception as exc: + logger.warning("bypass-lesson upsert fehlgeschlagen: %s", exc) except Exception as exc: logger.warning("api_heuristic fehlgeschlagen: %s", exc) diff --git a/aria-brain/api_heuristic.py b/aria-brain/api_heuristic.py index 7b7fc8f..3fa6492 100644 --- a/aria-brain/api_heuristic.py +++ b/aria-brain/api_heuristic.py @@ -78,6 +78,99 @@ def invalidate_cache() -> None: _cache.update(computed_at=0.0, hints=[]) +def detect_recent_bypass( + existing_skills: list[dict], + since_sec: int = 600, +) -> list[dict]: + """Findet Skill-Bypass-Vorfaelle: Bash-curl gegen einen Host fuer den + bereits ein matching Skill existiert. ARIA haette `run_` nutzen + sollen, hat aber gecurled. Das ist Drift — wir wollen es Brain merken. + + Returns: liste {host, skill_name, count, last_ts} fuer Hosts wo ein + Bypass in den letzten `since_sec` Sekunden vorkam. + """ + if not AGENT_STREAM_LOG.exists() or not existing_skills: + return [] + cutoff_ms = (time.time() - since_sec) * 1000 + # Map host → matching skill_name + host_to_skill = {} + for s in existing_skills: + sname = (s.get("name") or "").lower() + if not sname: + continue + # Heuristik wie in _host_already_has_skill: stem des Skill-Namens + # mit Hostnamen verglichen. Fuer scaffolded skills nehmen wir den + # Skill-Namen als stem (z.B. "spotify" -> matched api.spotify.com) + host_to_skill[sname] = sname + + bypass_events: dict[str, dict] = {} + try: + with AGENT_STREAM_LOG.open(encoding="utf-8") as f: + for line in f: + if not line.strip(): + continue + try: + e = json.loads(line) + except Exception: + continue + if e.get("kind") != "tool_use": + continue + if (e.get("name") or "") != "Bash": + continue + ts = e.get("ts") or 0 + if ts < cutoff_ms: + continue + for host in _extract_hosts_from_bash_input(e.get("input") or ""): + h = host.lower() + if h in _IGNORED_HOSTS: + continue + # Welcher Skill-Name matched diesen Host? + matched_skill = None + for skill_stem in host_to_skill: + if skill_stem in h: + matched_skill = host_to_skill[skill_stem] + break + if not matched_skill: + continue + entry = bypass_events.setdefault(h, { + "host": h, "skill_name": matched_skill, + "count": 0, "last_ts": 0, + }) + entry["count"] += 1 + if ts > entry["last_ts"]: + entry["last_ts"] = ts + except Exception as exc: + logger.warning("detect_recent_bypass: konnte log nicht lesen: %s", exc) + return [] + return list(bypass_events.values()) + + +def build_bypass_section(bypass_events: list[dict]) -> str: + """Drastischer Block fuer den System-Prompt wenn ARIA gerade gegen einen + Host gecurled hat OBWOHL der Skill existiert. Inhalt soll sie spuerbar + ermahnen — wirkt nur in der aktuellen Session.""" + if not bypass_events: + return "" + lines = [ + "## 🚨 SKILL-BYPASS ERKANNT", + "", + "Du hast gerade — IN DEN LETZTEN MINUTEN — Bash-curl gegen Hosts " + "gemacht obwohl ein passender Skill existiert. Das ist Verschwendung: " + "5 Bash-Roundtrips à 3s statt 1 Tool-Call à 3s. Stefan wartet doppelt. " + "AB JETZT in diesem Chat:", + "", + ] + for ev in bypass_events: + sname = ev["skill_name"] + host = ev["host"] + count = ev["count"] + lines.append(f"- gegen **{host}** ({count}x kuerzlich) → nutze " + f"`run_{sname.replace('-', '_')}(...)` statt curl. " + f"Der Skill ist da. Nutze ihn.") + lines.append("") + return "\n".join(lines) + + def _extract_hosts_from_bash_input(input_str: str) -> list[str]: """Hostnames aus URLs in einem Bash-Command. Sehr robust — sucht `https?://host`.""" if not input_str: