diff --git a/aria-brain/seed_rules.py b/aria-brain/seed_rules.py index 24d852d..8c20e2e 100644 --- a/aria-brain/seed_rules.py +++ b/aria-brain/seed_rules.py @@ -602,6 +602,61 @@ SEED_RULES: List[dict] = [ "'API Key' im Auth-Kapitel). Nicht raten." ), }, + { + "migration_key": "seed/skill-rule/list-api-pagination-snapshot", + "type": "rule", + "title": "Listen-API: einmal vollstaendig laden, DANN entscheiden", + "category": "verhalten", + "content": ( + "Wenn ein Tool-Resultat ein Pagination-Schema hat (limit/offset/" + "next oder total > limit): ALLE Seiten in EINEM Tool-Call holen, " + "in EINEM Snapshot durchsuchen, ERST DANN handeln.\n" + "\n" + "Antipattern (31.05.2026, Stefan reproduziert mit 'Playlist Prodigy " + "raussuchen'):\n" + " - run_spotify path=/v1/me/playlists?limit=50\n" + " → 'nicht dabei'\n" + " - run_spotify path=/v1/me/playlists?limit=50&offset=50\n" + " → 'gefunden, ID=X' (46 Tracks)\n" + " - run_spotify path=/v1/me/player/play body={context_uri: ...:X}\n" + " → spielt aber FALSCHE Playlist\n" + " - Neue Suche, wieder paginiert → drittes Match ID=Y (15 Tracks)\n" + " - Insgesamt drei verschiedene IDs fuer dieselbe gesuchte Playlist\n" + " generiert, am Ende die falsche gespielt.\n" + "\n" + "Wurzel: Spotify sortiert /v1/me/playlists nach recently-played. " + "Zwischen aufeinanderfolgenden paginierten Calls AENDERT SICH die " + "Reihenfolge wenn parallel was abgespielt wird. Teilresultate aus " + "verschiedenen Calls vergleichen → inkonsistent.\n" + "\n" + "Richtig fuer Spotify (seit 31.05.2026 unterstuetzt):\n" + " run_spotify path=/v1/me/playlists?limit=50&_all=true\n" + " → Skill paginiert intern, liefert {items, total, fetched_count}.\n" + " → In items[] suchen, EINE ID waehlen, sofort handeln.\n" + " → Match-Logik: bevorzugt exakter Name (case-insensitive). " + "Wenn mehrere Substring-Matches: explizit nachfragen statt raten.\n" + "\n" + "Wann _all=true sinnvoll:\n" + " - /v1/me/playlists (alle eigenen Playlists)\n" + " - /v1/playlists/{id}/tracks (alle Tracks einer Playlist)\n" + " - /v1/me/tracks (Liked Songs)\n" + " - /v1/search?type=playlist&q=... (Such-Ergebnisse mit next)\n" + " - Andere Endpunkte mit items+next-Schema.\n" + "\n" + "Wann NICHT _all=true:\n" + " - /v1/me/player/currently-playing (kein Listen-Endpunkt)\n" + " - /v1/me/player/devices (kurze Liste, kein next)\n" + " - Wenn Du explizit nur 'die ersten 10' willst.\n" + "\n" + "Fuer andere Skills (yt-dlp, andere APIs) die noch kein _all " + "unterstuetzen: manuell paginieren bis total erreicht, ALLES in " + "EINEM mentalen Snapshot mergen, NIEMALS auf Teilresultaten " + "Entscheidungen treffen. Wenn zwei Pagination-Runs unterschiedliche " + "Matches liefern: ehrlich melden ('zwei verschiedene Playlists " + "namens X gefunden — welche meinst Du?') statt sich auf eine " + "festzulegen." + ), + }, ] diff --git a/aria-brain/skills.py b/aria-brain/skills.py index eb5d465..acc3cf9 100644 --- a/aria-brain/skills.py +++ b/aria-brain/skills.py @@ -683,8 +683,13 @@ def run_skill(name: str, args: Optional[dict] = None, timeout_sec: int = 300) -> timed_out = True duration = time.time() - t0 - # Log schreiben (gekuerzt damit es nicht explodiert) - record = { + # Log auf der Disk wird gekuerzt (8000 chars) — sonst sammeln sich + # logs/*.json mit MBs an grossen Skill-Outputs an. Der Return-Value + # an den Caller (Agent) bekommt aber den vollen Output, dort wird + # nochmal in agent.py auf 50000 gecappt. Stefan-Fall: spotify-Skill + # mit _all=true liefert 50+ KB JSON, das hier wurde vorher auf 8 KB + # gekappt → ARIA sah immer nur den Anfang der Liste. + log_record = { "ts": _now(), "args": args or {}, "exit_code": exit_code, @@ -694,7 +699,7 @@ def run_skill(name: str, args: Optional[dict] = None, timeout_sec: int = 300) -> "timed_out": timed_out, } try: - log_path.write_text(json.dumps(record, indent=2, ensure_ascii=False), encoding="utf-8") + log_path.write_text(json.dumps(log_record, indent=2, ensure_ascii=False), encoding="utf-8") except Exception: pass @@ -703,8 +708,19 @@ def run_skill(name: str, args: Optional[dict] = None, timeout_sec: int = 300) -> manifest["use_count"] = int(manifest.get("use_count", 0)) + 1 write_manifest(name, manifest) - record["ok"] = exit_code == 0 - record["log_path"] = str(log_path) + # Return-Value: nicht kuerzen (Agent kuerzt downstream selbst). Nur + # die Disk-Log-Variante war beschnitten. + record = { + "ts": log_record["ts"], + "args": log_record["args"], + "exit_code": exit_code, + "duration_sec": log_record["duration_sec"], + "stdout": out_text or "", + "stderr": err_text or "", + "timed_out": timed_out, + "ok": exit_code == 0, + "log_path": str(log_path), + } return record