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:
+79
-6
@@ -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."
|
||||
|
||||
+71
-5
@@ -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("")
|
||||
|
||||
Reference in New Issue
Block a user