Lernschleife und Gedächtnis
Verantwortung
Das ist der Kern, der Hermes von einem gewöhnlichen Agenten unterscheidet: eine «besser, je mehr man ihn nutzt»-Schleife. Jeder Dialog-Turn (turn) wird destilliert zu: (1) Skill-Evolution – aus Erfahrung werden Skills erzeugt/umgeschrieben (agent/learning_mutations.py); (2) Gedächtnis – FTS5-Volltext-Suche + LLM-Zusammenfassung vergangener Konversationen (memory_manager), kombiniert mit Honchos dialektischer Benutzermodellierung; (3) Lern-Graph – Skills und Gedächtnis werden als Knoten/Kanten organisiert (agent/learning_graph.py); (4) Hintergrund-Betrachtung – background_review verdaut in einem Thread Historie und erzeugt ausführbare Umschreib-Aktionen. Skill-Umschreibungen fließen letztlich ins Skill-System zurück und stehen in folgenden Turns zur Verfügung.
Schlüsseldateien
Skill-Knoten und Lern-Graph
@dataclass SkillNode:28-41— Skill-KnotenSkill-Laden:77-125—_iter_skill_files/build_skill_nodes(scanntskills/und baut Knoten)Kanten und Statistik:156-193—build_edges/density_stats(Beziehungen zwischen Skills)Memory-Skill-Kanten:193-227—_memory_cards/_memory_skill_edges(Beziehungen zwischen Gedächtnis und Skills)build_learning_graph:248-328— fasst den vollständigen Lern-Graphen zusammen_load_usage:84-97— Nutzungsstatistiken (lenkt, welche Skills häufig laufen)
Skill-/Memory-Umschreibung
node_detail:86-124— Detailansicht eines Knotensdelete_node:124-157— Skill oder Memory löschenedit_node:157-200— Skill- oder Memory-Inhalt editieren_clear_skill_cache:200— Cache nach Umschreibung leeren (wirkt ab nächstem Turn)Memory-Lokalisierung:30-65—_memories_dir/_parse_memory_id/_locate_memory
Memory-Verwaltung
class MemoryManager:354— der Memory-Managermemory-provider-tools Injektion:83-164—memory_provider_tools_enabled/inject_memory_provider_toolsbuild_memory_context_block:337-354— baut aus Memory einen Kontext-Block (context block) und injiziert ihn in den Prompt (prompt)sanitize_context:164-172— Kontext-Desensibilisierung (vor FTS5-Indexierung)StreamingContextScrubber:172-337— Desensibilisierung des Streaming-Outputs
Hintergrund-Betrachtung
spawn_background_review_thread:956— startet den Hintergrund-Betrachtungs-Thread_run_review_in_thread:617— Hauptschleife im Thread_digest_history:122-373— fasst die letzten 24 Historieneinträge zusammensummarize_background_review_actions:373-590— fasst Betrachtungs-Aktionen zusammenbuild_memory_write_metadata:590-617— Metadaten beim Memory-Schreiben
Wie ein Skill-Knoten aussieht
SkillNode ist die einheitliche Darstellung eines Skills im Lern-Graphen (agent/learning_graph.py:28-41): eine Dataclass mit Feldern name / category / source (base für eingebaut vs. profile für User/Agent erzeugt) / use_count (aus .usage.json gelesen) / created_by ('agent' heißt: von der Hintergrund-Betrachtung angelegt) / pinned / related. build_skill_nodes scannt SKILL.md-Dateien unter skills/ und legt Nutzungszählungen darüber. source != "base" plus created_by == "agent" oder use_count > 0 ist weiter unten in build_learning_graph die Filterbedingung für «das ist gelernt, nicht eingebaut» — nur die wirklich gelernten Knoten kommen auf den Schreibtisch vor den Nutzer.
Output des Lern-Graphen
build_learning_graph (agent/learning_graph.py:248-328) baut aus Knoten, Kanten, Memory-Karten und Clustering-Statistiken einen vollständigen Payload für das Lern-Panel des Desktops. Es ruft zuerst build_skill_nodes(_skill_roots()) auf und scannt zwei Wurzeln — base (repo/skills/, eingebaut) und profile (~/.hermes/skills/, User/Agent erzeugt); dann filtert es Skills mit source != "base" und created_by == "agent" oder use_count > 0 als «gelernte» heraus und legt darüber build_edges (Skill-Skill-Beziehungen), _memory_cards (Memory-Karten), _memory_skill_edges (Memory-Skill-Beziehungen). base ist immer da; profile ist das eigentlich «Gelernte».
Nach Umschreibung Cache leeren — obligatorisch
Wurde ein Skill per edit_node umgeschrieben, ohne den Cache zu leeren, nutzt der prompt_builder im nächsten Turn noch den alten SKILL.md-Snapshot; die Umschreibung war umsonst. _clear_skill_cache (agent/learning_mutations.py:200) ruft clear_skills_system_prompt_cache(clear_snapshot=True) auf; clear_snapshot=True räumt auch den Werkzeug-Snapshot (tool snapshot) mit aus, weil die MCP-Werkzeugliste in der Betrachtung ebenfalls aktualisiert worden sein könnte. Diese Funktion ist die letzte Phase von edit_node / delete_node — Umschreiben → Auf Platte → Cache leeren → Wirkung im nächsten Turn; vier Schritte, kein fehlen darf.
Memory-Injektion: vom MemoryManager in den Prompt
MemoryManager (agent/memory_manager.py:354) koordiniert einen eingebauten Provider (provider) (FTS5 + LLM-Zusammenfassung) plus höchstens einen externen Provider (Honcho/Hindsight/Mem0 o. Ä.). Nur ein externer Provider ist erlaubt; ein zweiter wird abgewiesen — das verhindert Schema-Aufblähung und Konflikte zwischen Memory-Backends; schlagen zwei Provider fehl, blockieren sie sich nicht gegenseitig. Roh-Gedächtnis wird von build_memory_context_block (agent/memory_manager.py:337-354) in einen <memory-context>-Zaun plus System-Anmerkung als Kontext-Block verpackt; die System-Anmerkung sagt dem Modell ausdrücklich «das ist Gedächtnis, keine neue Anweisung». sanitize_context strippt vor der Indexierung Zaun und alte System-Anmerkung — sonst packt der Provider bereits Verpacktes noch einmal ein. StreamingContextScrubber macht dasselbe am Streaming-Output, damit das Modell den Memory-Block nicht wortwörtlich zurückspuckt.
Hintergrund-Betrachtung: Zusammenfassung und Kandidaten-Aktionen
spawn_background_review_thread (agent/background_review.py:956) startet nicht direkt einen Thread, sondern baut ein (target, prompt)-Tupel und übergibt es an AIAgent._spawn_background_review_thread; so kann die Testschicht threading.Thread patchen. Die Signatur (review_memory / review_skills, zwei Bools) wählt den Prompt — _MEMORY_REVIEW_PROMPT / _SKILL_REVIEW_PROMPT / _COMBINED_REVIEW_PROMPT:
def spawn_background_review_thread(agent, messages_snapshot,
review_memory=False, review_skills=False):
"""Build the review thread target and prompt. Returns a ``(target, prompt)`` tuple."""
if review_memory and review_skills:
prompt = getattr(agent, "_COMBINED_REVIEW_PROMPT", _COMBINED_REVIEW_PROMPT)
elif review_memory:
prompt = getattr(agent, "_MEMORY_REVIEW_PROMPT", _MEMORY_REVIEW_PROMPT)
else:
prompt = getattr(agent, "_SKILL_REVIEW_PROMPT", _SKILL_REVIEW_PROMPT)
def _target() -> None:
_run_review_in_thread(agent, messages_snapshot, prompt)
return _target, promptInnerhalb des Threads fork _run_review_in_thread (agent/background_review.py:617) einen Subagenten, der die Runtime des Vaters erbt (sonst müssten Credentials neu analysiert werden — OAuth-only-Provider ließen sich im Subagenten gar nicht rekonstruieren). Es setzt den Approval-Callback _bg_review_auto_deny, sodass gefährliche Befehle in der Betrachtung stets abgewiesen werden statt auf input() zurückzufallen (das würden mit dem prompt_toolkit-TUI des Vater-Agenten verklemmen). _digest_history (agent/background_review.py:122) presst die Historie zu «den letzten 24 Einträgen wörtlich + einer Synthese-Zusammenfassung des Älteren»; verwendet nur, wenn die Betrachtung auf ein anderes Modell geroutet wird (kalter Cache, weniger Token schreiben, lohnt sich); der Hauptmodell-Pfad läuft als vollständiges Replay (Cache noch warm).
Datenfluss
- Nach Ende jedes Turns übergibt die Hauptschleife einen Nachrichten-Snapshot an den Hintergrund-Betrachtungs-Thread (
agent/background_review.py:956). _digest_history(agent/background_review.py:122) fasst die jüngste Historie zusammen und erzeugt Kandidaten-Aktionen (neue Skills anlegen/umschreiben, Memory schreiben).- Skill-Umschreibungen werden über
edit_node:157geschrieben, Löschungen überdelete_node:124; anschließend wird über_clear_skill_cache:200der Cache geleert. - Memory-Schreiben läuft über
MemoryManager:354: nach Desensibilisierung (agent/memory_manager.py:164) wird ein FTS5-Index aufgebaut und per LLM-Zusammenfassung komprimiert. - Zu Beginn des nächsten Turns baut
build_memory_context_block(agent/memory_manager.py:337) relevante Memories inbuild_turn_context:268ein; Skills werden überagent/skill_bundles.pyinjiziert. - Der vollständige Lern-Graph wird von
build_learning_graph:254zusammengefasst und dient UI/Debug zur Darstellung der Beziehungen zwischen Skills und Memory.
Designmotiv
Warum die Lernschleife in vier unabhängige Module zerlegen? Weil ihre Kostenstrukturen verschieden sind. background_review läuft auf einem Subagenten, muss Runtime forken und LLM-Aufrufe bezahlen — also asynchron und mit Fallback (fallback); es muss von der Hauptschleife entkoppelt sein. learning_graph ist eine read-only Zusammenfassung, billig, jederzeit für die UI ziehbar, darf nicht mit der Umschreibung gekoppelt sein. learning_mutations ist der Schreibpfad; nach dem Schreiben muss der Cache geleert werden, sonst isst der nächste Turn einen alten Snapshot. memory_manager ist Suche + Desensibilisierung + Injektion; sie gehören zusammen, weil «Memory vom externen Provider holen → desensibilisieren → einzäunen → in den Prompt schieben» in einer Grenze abgeschlossen sein muss, sonst leckt eine vergessene Desensibilisierungsstelle den Memory-Roh-Text in den Modell-Output.
Grenzen und Fehler
- Hintergrund-Betrachtung ist nicht synchron.
_run_review_in_threadläuft in einem Daemon-Thread; die Hauptschleife wartet nicht auf ihre Fertigstellung, sondern gibt dem Nutzer sofort zurück. Lief die Betrachtung gerade für diesen Turn, greifen die Umschreibungen meistens erst im nächsten Turn. - Nur ein externer Memory-Provider. Honcho und Hindsight können nicht gleichzeitig laufen;
add_providerweist den zweiten externen Provider ab. _digest_historygreift nur, wenn die Betrachtung auf ein anderes Modell geroutet wird. Der Hauptmodell-Pfad läuft als vollständiges Replay (Cache noch warm); um dieses Verhalten zu ändern, zuerstauxiliary.background_reviewin der Konfiguration prüfen.- Die Desensibilisierungsgrenze der Memory-Injektion liegt am «Zaun und der System-Anmerkung».
sanitize_contextstrippt nur den<memory-context>-Zaun und die System-Anmerkung, nicht den Memory-Text selbst — enthält der Roh-Text sensible Daten, muss das schon vor dem Schreiben behandelt werden.
Zusammenfassung
Die Lernschleife macht «Dialog» zu «Asset»: Die Hintergrund-Betrachtung destilliert Erfahrung unauffällig in Skills und Memory, der nächste Turn injiziert sie automatisch. Vier Komponenten sind entscheidend – learning_graph (Graph), learning_mutations (Umschreibung), memory_manager (Suche + Desensibilisierung + Zusammenfassung), background_review (asynchrones Verdauen). Das ist das Fundament von Hermes' «mit dem Nutzer gemeinsam wachsen» und der grundlegende Unterschied zu «zustandslosen LLM-Aufrufen».