feat(brain): ARIA pflegt ihr Gedaechtnis selbst — Anleitung, Merkzeilen, Loeschen, ehrlichere Destillation

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 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 09:19:34 +01:00
co-authored by Claude Opus 5.5
parent 4d590df866
commit f479a0781d
2 changed files with 150 additions and 11 deletions
+79 -6
View File
@@ -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."