From f479a0781d1bdec18a8c6e531e52f0136d86cb22 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Wed, 28 Oct 2026 09:19:34 +0100 Subject: [PATCH] =?UTF-8?q?feat(brain):=20ARIA=20pflegt=20ihr=20Gedaechtni?= =?UTF-8?q?s=20selbst=20=E2=80=94=20Anleitung,=20Merkzeilen,=20Loeschen,?= =?UTF-8?q?=20ehrlichere=20Destillation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anlass: Im Projekt Software Bude hat ARIA Test- (aria-wohnung) und Prod-Schmiede (172.0.2.46) verwechselt, richtige Berichte fuer erfunden erklaert und auf der Testumgebung ein Projekt angelegt. Ihr Wissen dazu lag ungepinnt und veraltet ("ALTE Testinstanz") im kalten Gedaechtnis. - Systemprompt: neuer Abschnitt "DEIN GEDAECHTNIS — so pflegst du es": von sich aus speichern, was nicht live nachsehbar ist; was NICHT rein gehoert (laufende Staende, eigene ungepruefte Berichte); Schubladen (tool = Infrastruktur, angepinnt); vor dem Speichern suchen, aktualisieren statt doppeln, Falsches loeschen; Erinnern != Wissen; jeder Stand-Bericht nennt Quelle (wo/womit/wann). - Verzeichnis: Titel + Merkzeile + Datum statt nur Titel; Cold-Treffer mit Datum. - memory_save: identity/rule/preference/tool/skill werden angepinnt wie beschrieben (vorher nur mit explizitem pinned=true — Infrastruktur landete kalt). memory_update kann die Schublade aendern. Neu: memory_delete. - Destillat: Fakten nur aus dem, was Stefan sagt/entscheidet/bestaetigt; keine live nachsehbaren Staende (Ticket-Nummern, Status, Commit-IDs). Co-Authored-By: Claude Opus 5.5 --- aria-brain/agent.py | 85 ++++++++++++++++++++++++++++++++++++++++--- aria-brain/prompts.py | 76 +++++++++++++++++++++++++++++++++++--- 2 files changed, 150 insertions(+), 11 deletions(-) diff --git a/aria-brain/agent.py b/aria-brain/agent.py index 7204f9e..9f71490 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -1016,6 +1016,27 @@ META_TOOLS = [ "category": {"type": "string", "description": "Neue Kategorie (optional)"}, "tags": {"type": "array", "items": {"type": "string"}, "description": "Neue Tags (ueberschreibt komplett)"}, "pinned": {"type": "boolean", "description": "Pinning aendern (optional)"}, + "type": {"type": "string", "enum": ["identity", "rule", "preference", "tool", "skill", "fact", "conversation", "reminder"], + "description": "Schublade aendern (optional), z.B. fact → tool fuer Infrastruktur. tool/rule/preference/skill/identity werden dabei angepinnt."}, + }, + "required": ["id"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "memory_delete", + "description": ( + "Loesche einen Gedaechtnis-Eintrag, der FALSCH oder UEBERHOLT ist " + "(ID aus memory_search oder Cold-Memory). Ist er nur unvollstaendig, " + "lieber memory_update. identity-Eintraege kann nur Stefan loeschen." + ), + "parameters": { + "type": "object", + "properties": { + "id": {"type": "string", "description": "Memory-ID (UUID)"}, + "reason": {"type": "string", "description": "Kurz warum (landet im Log)"}, }, "required": ["id"], }, @@ -1027,8 +1048,10 @@ META_TOOLS = [ "name": "memory_save", "description": ( "Speichere eine Information dauerhaft in deinem Gedaechtnis (Qdrant-DB). " - "Nutze das wenn Stefan 'merk dir das' sagt oder du selbst etwas Wichtiges " - "festhalten willst. ALTERNATIVEN VERMEIDEN: du hast KEIN persistentes " + "Nutze das VON DIR AUS, sobald du etwas Nicht-Offensichtliches lernst " + "(siehe 'DEIN GEDAECHTNIS' im Prompt) — nicht erst auf 'merk dir das'. " + "VORHER memory_search: gibt es schon einen Eintrag, nimm memory_update. " + "Keine live nachsehbaren, sich aendernden Staende speichern. ALTERNATIVEN VERMEIDEN: du hast KEIN persistentes " "File-Memory mehr — schreibe nicht in `~/.claude/projects/...`, das ist tot.\n\n" "Type-Wahl:\n" "- identity: ARIAs Selbstbild / Wesensart (PINNED)\n" @@ -1072,7 +1095,7 @@ META_TOOLS = [ }, "category": {"type": "string", "description": "Optional, freier Tag z.B. 'meine-sachen', 'kunden', 'persoenlichkeit'"}, "tags": {"type": "array", "items": {"type": "string"}, "description": "Optionale Tags"}, - "pinned": {"type": "boolean", "description": "Default false. Nur true wenn die Info IMMER im System-Prompt liegen muss (Identitaet/Regeln/Praeferenzen)."}, + "pinned": {"type": "boolean", "description": "Weglassen = automatisch nach Schublade (identity/rule/preference/tool/skill angepinnt, fact nicht). Nur angeben, um davon abzuweichen."}, "attach_paths": { "type": "array", "items": {"type": "string"}, @@ -2372,6 +2395,9 @@ class Agent: # 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). + # Schubladen, die angepinnt sind (immer im System-Prompt) + _PINNED_TYPES = {"identity", "rule", "preference", "tool", "skill"} + _SOFT_FAIL_MARKERS = ( "fehler", "fehlgeschlagen", "nicht gefunden", "not found", "konnte nicht", "keine verbindung", "nicht verfuegbar", @@ -4405,6 +4431,15 @@ class Agent: existing.category = str(arguments["category"]).strip() if "tags" in arguments and arguments["tags"] is not None: existing.tags = [str(t).strip() for t in (arguments["tags"] or []) if str(t).strip()] + if arguments.get("type"): + new_type = str(arguments["type"]).strip() + if new_type not in {"identity", "rule", "preference", "tool", "skill", + "fact", "conversation", "reminder"}: + return f"FEHLER: unbekannter type {new_type!r}." + if new_type != existing.type: + existing.type = new_type + if arguments.get("pinned") is None: + existing.pinned = new_type in self._PINNED_TYPES if "pinned" in arguments and arguments["pinned"] is not None: existing.pinned = bool(arguments["pinned"]) existing.updated_at = _dt.datetime.now(_dt.timezone.utc).isoformat() @@ -4432,6 +4467,29 @@ class Agent: except Exception as e: logger.exception("memory_update fehlgeschlagen") return f"FEHLER: {e}" + if name == "memory_delete": + pid = (arguments.get("id") or "").strip() + reason = (arguments.get("reason") or "").strip() + if not pid: + return "FEHLER: id ist Pflicht." + existing = self.store.get(pid) + if not existing: + return f"FEHLER: Memory mit id={pid[:8]} nicht gefunden." + if existing.type == "identity": + return "FEHLER: identity-Eintraege loescht nur Stefan (Diagnostic)." + try: + self.store.delete(pid) + logger.info("[memory] geloescht: %r (%s) — Grund: %s", existing.title, pid[:8], reason or "-") + self._pending_events.append({ + "type": "memory_saved", "action": "deleted", + "memory": {"id": pid, "type": existing.type, "title": existing.title, + "content_preview": (existing.content or "")[:140], + "category": existing.category, "pinned": existing.pinned}, + }) + return f"OK — Memory '{existing.title}' geloescht." + except Exception as e: + logger.exception("memory_delete fehlgeschlagen") + return f"FEHLER: {e}" if name == "memory_save": title = (arguments.get("title") or "").strip() content = (arguments.get("content") or "").strip() @@ -4445,7 +4503,13 @@ class Agent: category = (arguments.get("category") or "").strip() tags_in = arguments.get("tags") or [] tags = [str(t).strip() for t in tags_in if str(t).strip()] if isinstance(tags_in, list) else [] - pinned = bool(arguments.get("pinned", False)) + # Schubladen identity/rule/preference/tool/skill sind laut + # Beschreibung ANGEPINNT — frueher nur, wenn pinned=true explizit + # mitkam (Default false), Infrastruktur landete so im kalten Teil. + if arguments.get("pinned") is None: + pinned = mem_type in self._PINNED_TYPES + else: + pinned = bool(arguments.get("pinned")) attach_paths_in = arguments.get("attach_paths") or [] attach_paths = [str(p).strip() for p in attach_paths_in if str(p).strip()] if isinstance(attach_paths_in, list) else [] try: @@ -4695,13 +4759,22 @@ class Agent: # Konversation als Klartext bauen transcript = "\n".join( - f"[{t.role.upper()}] {t.content}" for t in old_turns + f"[{'STEFAN' if t.role == 'user' else 'ARIA'}] {t.content}" for t in old_turns )[:30000] # Cap auf 30k Zeichen damit der Prompt nicht explodiert system = ( "Du extrahierst aus einer Konversation zwischen Stefan und ARIA die " "wichtigsten dauerhaft relevanten Fakten — keine Smalltalk-Details, " - "keine flüchtigen Zustände. Antworte AUSSCHLIESSLICH mit gültigem JSON " + "keine flüchtigen Zustände.\n" + "QUELLE: Fakten kommen aus dem, was STEFAN sagt, entscheidet oder " + "bestätigt. ARIAs Nachrichten sind Berichte und können falsch sein — " + "übernimm daraus nur, was Stefan danach bestätigt hat.\n" + "NICHT aufnehmen: Systemzustände, die man live nachsehen kann und die " + "sich ändern (Projekt-/Ticket-Nummern im Fluss, Ticket-Status, Zeilen-" + "zahlen, Commit-IDs, wer gerade woran arbeitet, 'gerade nachgesehen'-" + "Befunde). Gut sind: Entscheidungen und ihr Grund, Stefans Vorlieben, " + "dauerhafte Regeln/Abläufe, wo etwas läuft (falls Stefan es bestätigt).\n" + "Antworte AUSSCHLIESSLICH mit gültigem JSON " "im Format: {\"facts\": [{\"title\": \"kurz, max 80 Zeichen\", " "\"content\": \"1-3 Sätze, konkret und nützlich\"}]}. " "Mindestens 0, höchstens 7 Facts. Wenn nichts wichtig genug ist: leeres Array." diff --git a/aria-brain/prompts.py b/aria-brain/prompts.py index 35484ed..c54873a 100644 --- a/aria-brain/prompts.py +++ b/aria-brain/prompts.py @@ -292,6 +292,70 @@ def build_hot_memory_section(pinned: List[MemoryPoint]) -> str: return "\n".join(parts).strip() +MEMORY_CARE = """## DEIN GEDAECHTNIS — so pflegst du es + +Zwischen zwei Nachrichten behaeltst du NICHTS. Was du spaeter wissen musst, steht +nur dann wieder vor dir, wenn es im Gedaechtnis liegt — angepinnt (immer im +Prompt), im Verzeichnis unten (Titel + Merkzeile) oder als Treffer der Suche. +Darum pflegst du es selbst, ohne dass Stefan "merk dir das" sagen muss. + +### Wann speichern (von dir aus) +- Du lernst etwas, das man sonst muehsam neu herausfinden muesste: wo etwas + laeuft, welches System Test und welches Produktion ist, wie man hinkommt, wie + ein Ablauf funktioniert, was Stefan entschieden hat und WARUM. +- Stefan korrigiert dich oder bestaetigt einen nicht offensichtlichen Weg. +- Sofort, nicht "spaeter" — am Ende des Gespraechs bist du es nicht mehr. + +### Was NICHT ins Gedaechtnis gehoert +- Was man jederzeit live nachsehen kann und sich laufend aendert: Ticket-Staende, + Projekt-/Ticket-Nummern im Fluss, Zeilenzahlen, "wer arbeitet gerade woran". + Das wird NACHGESEHEN, nicht erinnert. +- Deine eigenen Berichte oder Vermutungen, die du nicht in diesem Zug geprueft hast. +- Was ohnehin im Code, in Git oder in der Doku steht. + +### Welche Schublade +- `tool` (angepinnt): Infrastruktur und Umgebungen — Hosts, Zugaenge, welche + Instanz Test und welche Produktion ist. Das muss IMMER vor dir liegen. +- `rule` / `preference` (angepinnt): harte Regeln, Stefans Arbeitsweise. +- `fact`: alles andere (nur per Suche sichtbar). Der Titel ist die Merkzeile im + Verzeichnis — schreib ihn so, dass du beim Lesen weisst, wann du nachschlagen + musst (z.B. "Schmiede: Prod 172.0.2.46, Test aria-wohnung" statt "Software Schmiede"). + +### Pflegen statt anhaeufen +- VOR dem Speichern mit `memory_search` pruefen, ob es schon einen Eintrag gibt. + Gibt es einen: `memory_update` statt eines zweiten Eintrags. +- Hat sich etwas geaendert (z.B. eine "alte" Kopie ist jetzt deine Testumgebung): + den Eintrag SOFORT berichtigen. +- Ist ein Eintrag falsch oder ueberholt: `memory_delete`. + +### Erinnern ist nicht Wissen +- Ein Eintrag gibt den Stand von damals wieder (Datum steht dabei). Bevor du + Stefan einen Systemzustand als Tatsache nennst, pruefe ihn live. +- Jeder Stand-Bericht nennt seine Quelle: WO und WOMIT du nachgesehen hast und + wann ("Prod-Board 172.0.2.46, gerade eben"). Hast du in diesem Zug nicht + nachgesehen, sag das — und schau nach, statt aus dem Verlauf weiterzuschreiben. +- Widerspricht ein neuer Befund einem frueheren aus dem Verlauf: erst klaeren, + WO du jeweils nachgesehen hast, bevor du einen davon fuer falsch erklaerst.""" + + +def build_memory_care_section() -> str: + return MEMORY_CARE + + +def _mem_date(p: MemoryPoint) -> str: + d = (p.updated_at or p.created_at or "")[:10] + return f" (Stand {d})" if d else "" + + +def _mem_hook(p: MemoryPoint, n: int = 110) -> str: + """Erste Zeile/Satz des Inhalts als Merkzeile fuers Verzeichnis.""" + c = " ".join((p.content or "").split()) + if len(c) <= n: + return c + cut = c[:n] + return cut[: cut.rfind(" ")] + " …" if " " in cut else cut + " …" + + def build_cold_memory_section(matches: List[MemoryPoint]) -> str: """Baue 'Moeglicherweise relevant'-Block aus Search-Treffern.""" if not matches: @@ -299,7 +363,7 @@ def build_cold_memory_section(matches: List[MemoryPoint]) -> str: lines = ["## Moeglicherweise relevant (aus Gedaechtnis)"] for p in matches: score = f" [score={p.score:.2f}]" if p.score is not None else "" - lines.append(f"- **{p.title}**{score}") + lines.append(f"- **{p.title}**{_mem_date(p)}{score}") lines.append(f" {p.content.strip()}") att_line = _attachments_line(p) if att_line: @@ -322,8 +386,8 @@ def build_memory_index_section(index_titles: List[MemoryPoint]) -> str: lines = [ "## Was in deinem Gedaechtnis liegt (per memory_search abrufbar)", - "Diese Eintraege hast DU gespeichert — hier nur die Titel, nicht der " - "Inhalt. Wenn einer zur Aufgabe passt, hol den Inhalt mit `memory_search` " + "Diese Eintraege hast DU gespeichert — hier Titel und Merkzeile, nicht der " + "ganze Inhalt. Wenn einer zur Aufgabe passt, hol den Inhalt mit `memory_search` " "(Titel oder Stichwort). **Frag Stefan NICHT nach etwas, das hier steht** " "(Zugangsdaten, Server/Hosts, Projekt-Stand, Konfig) — erst nachsehen.", "", @@ -332,7 +396,8 @@ def build_memory_index_section(index_titles: List[MemoryPoint]) -> str: items = grouped[cat] lines.append(f"### {cat}") for p in items: - lines.append(f"- {p.title}") + hook = _mem_hook(p) + lines.append(f"- {p.title}{_mem_date(p)}" + (f" — {hook}" if hook and hook != p.title else "")) lines.append("") return "\n".join(lines).strip() @@ -555,7 +620,8 @@ def build_system_prompt( """Kompletter System-Prompt: Hot + Cold + Skills + Triggers + FLUX + OAuth.""" # Identitaets-Anker IMMER zuerst — vor allen Memories/Sektionen, damit die # ARIA-Rolle auch in Projekten mit injection-artigem Inhalt (Pentest) haelt. - parts = [IDENTITY_ANCHOR, "", build_hot_memory_section(pinned), "", build_time_section(), + parts = [IDENTITY_ANCHOR, "", build_hot_memory_section(pinned), "", build_memory_care_section(), + "", build_time_section(), "", build_voice_flow_section()] if skills: parts.append("")