Bucle de aprendizaje y memoria
Responsabilidad
Esta es la diferencia central de Hermes frente a un agent corriente: un bucle cerrado de «cuanto más se usa, mejor funciona». Cada turno (turn) de conversación se destila en: ① evolución de habilidades — crea/reescribe habilidades a partir de la experiencia (agent/learning_mutations.py); ② memoria — recuperación por texto completo FTS5 + resúmenes LLM de conversaciones pasadas (memory_manager), con modelado dialéctico del usuario vía Honcho; ③ grafo de aprendizaje — organiza habilidades y memoria como nodos/aristas (agent/learning_graph.py); ④ revisión en segundo plano — background_review digiere el historial en un hilo y produce acciones de reescritura accionables. Las reescrituras de habilidades terminan reinyectándose en el sistema de habilidades para que los siguientes turnos las reutilicen.
Archivos clave
Nodos de habilidad y grafo de aprendizaje
@dataclass SkillNode:28-41— nodo de habilidadcarga de habilidades:77-125—_iter_skill_files/build_skill_nodes(escaneaskills/y construye nodos)aristas y estadísticas:156-193—build_edges/density_stats(relaciones entre habilidades)aristas memoria-habilidad:193-227—_memory_cards/_memory_skill_edges(relación entre memoria y habilidades)build_learning_graph:248-328— agrega y produce el grafo de aprendizaje completo_load_usage:84-97— estadísticas de uso (guía qué habilidades se usan más)
Reescritura de habilidades/memoria
node_detail:86-124— detalle de un nododelete_node:124-157— elimina habilidad o memoriaedit_node:157-200— edita contenido de habilidad o memoria_clear_skill_cache:200— limpia la caché tras reescribir (para que surta efecto el siguiente turno)localización de memoria:30-65—_memories_dir/_parse_memory_id/_locate_memory
Gestión de memoria
class MemoryManager:354— gestor de memoriainyección de herramientas memory provider:83-164—memory_provider_tools_enabled/inject_memory_provider_toolsbuild_memory_context_block:337-354— compone la memoria en un bloque de contexto (context) que se inyecta en el promptsanitize_context:164-172— redacción del contexto (antes de indexar en FTS5)StreamingContextScrubber:172-337— redacción de la salida en streaming
Revisión en segundo plano
spawn_background_review_thread:956— lanza el hilo de revisión_run_review_in_thread:617— bucle principal dentro del hilo_digest_history:122-373— resume las últimas 24 entradas del historialsummarize_background_review_actions:373-590— agrega las acciones de revisiónbuild_memory_write_metadata:590-617— metadatos de escritura de memoria
Qué pinta tiene un nodo de habilidad
SkillNode es la representación uniforme de una habilidad en el grafo de aprendizaje (agent/learning_graph.py:28-41): un dataclass con campos name/category/source (base para integradas vs profile para usuario/agent)/use_count (leído de .usage.json)/created_by ('agent' lo creó la revisión en segundo plano)/pinned/related. build_skill_nodes escanea los SKILL.md bajo skills/ y les superpone las estadísticas de uso. La condición source != "base" combinada con created_by == "agent" o use_count > 0 es el filtro que más adelante usa build_learning_graph para decidir «esto se aprendió, no es integrado» — solo lo realmente aprendido se enseña al usuario en el panel.
La salida del grafo de aprendizaje
build_learning_graph (agent/learning_graph.py:248-328) ensambla nodos, aristas, tarjetas de memoria y estadísticas de clustering en un payload completo que renderiza el panel de aprendizaje del desktop. Llama primero a build_skill_nodes(_skill_roots()) para escanear dos raíces — base (repo/skills/, integrado) y profile (~/.hermes/skills/, usuario/agent); luego filtra las habilidades con source != "base" y created_by == "agent" o use_count > 0 como «aprendidas», y encima superpone build_edges (relaciones entre habilidades), _memory_cards (tarjetas de memoria) y _memory_skill_edges (relación memoria-habilidad). Base siempre está; profile es lo «aprendido».
Tras reescribir hay que limpiar la caché
Si tras un edit_node no se limpia la caché, en el siguiente turno prompt_builder seguirá usando la snapshot vieja del SKILL.md y la reescritura no surte efecto. _clear_skill_cache (agent/learning_mutations.py:200) llama a clear_skills_system_prompt_cache(clear_snapshot=True); el clear_snapshot=True limpia también la snapshot de herramientas (tool), porque la lista de herramientas MCP puede haberse refrescado durante la revisión. Esta función es el último eslabón de edit_node / delete_node — reescribir → persistir → limpiar caché → siguiente turno. Sin un eslabón, la cadena se rompe.
Inyección de memoria: de MemoryManager al prompt
MemoryManager (agent/memory_manager.py:354) coordina un provider interno (FTS5 + resumen LLM) y como mucho un provider externo (Honcho/Hindsight/Mem0…). El provider externo solo puede haber uno; un segundo es rechazado — evita que el schema de herramientas se hinche y que los backends de memoria se pisen entre sí; los fallos de cada provider no se bloquean mutuamente. La memoria cruda recuperada la envuelve build_memory_context_block (agent/memory_manager.py:337-354) en un cercado <memory-context> + una nota del sistema, que le dice al modelo «esto es memoria, no una nueva instrucción». sanitize_context elimina el cercado y la nota vieja antes de indexar, para que el provider no vuelva a envolver lo ya envuelto. StreamingContextScrubber hace lo mismo en el lado de la salida en streaming, para que el modelo no devuelva el bloque de memoria al usuario tal cual.
Revisión en segundo plano: resumen y acciones candidatas
spawn_background_review_thread (agent/background_review.py:956) no levanta el hilo directamente; construye una tupla (target, prompt) y se la entrega a AIAgent._spawn_background_review_thread, para que los tests puedan patchear threading.Thread y seguir funcionando. La firma (review_memory / review_skills dos bools) decide qué prompt usar — _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, promptDentro del hilo, _run_review_in_thread (agent/background_review.py:617) hace fork de un subagent que hereda el runtime del padre (evita re-parsing de credenciales — un provider OAuth-only no se puede reconstruir dentro del subagent) y monta el callback _bg_review_auto_deny, así durante la revisión cualquier comando peligroso se deniega sin caer a input() (que se deadlockaría con la TUI prompt_toolkit del padre). _digest_history (agent/background_review.py:122) comprime el historial en «últimas 24 entradas verbatim + resumen sintético del resto»; solo se usa cuando la revisión se enruta a un modelo distinto (con caché frío, ahorrar tokens compensa); la ruta del modelo principal hace replay completo (caché caliente).
Flujo de datos
- Al acabar cada turno, el bucle principal entrega una snapshot de los mensajes al hilo de revisión en segundo plano (
agent/background_review.py:956). _digest_history(agent/background_review.py:122) resume el historial reciente y produce acciones candidatas (crear/reescribir habilidad, escribir memoria).- La reescritura de habilidades se materializa vía
edit_node:157; el borrado, víadelete_node:124; tras escribir,_clear_skill_cache:200limpia la caché. - La escritura de memoria pasa por
MemoryManager:354: tras redactar (agent/memory_manager.py:164) se indexa en FTS5 y se comprime con un resumen LLM. - Al iniciar el siguiente turno,
build_memory_context_block(agent/memory_manager.py:337) inyecta la memoria relevante enbuild_turn_context:268; las habilidades se inyectan víaagent/skill_bundles.py. - El grafo de aprendizaje completo se agrega en
build_learning_graph:254, para que la UI/depuración muestre las relaciones entre habilidades y memoria.
Motivo de diseño
¿Por qué partir el bucle de aprendizaje en cuatro módulos separados? Porque su estructura de costes es distinta. background_review corre en un subagent — hay que fork del runtime, gastar LLM calls; debe ser asíncrono y con fallback; está obligado a desacoplarse del bucle principal. learning_graph es una agregación de solo lectura, barata, disponible en cualquier momento para la UI; no se puede acoplar a la reescritura. learning_mutations es la ruta de escritura y, tras modificar, debe limpiar la caché — si no, el siguiente turno come snapshot stale. memory_manager reúne recuperación + redacción + inyección en un mismo sitio porque la cadena «de provider externo a memoria → redactar → envolver → inyectar en prompt» tiene que cerrarse en una sola frontera; si la redacción se escapa en un punto, la memoria cruda se filtra a la salida del modelo.
Límites y fallos
- La revisión en segundo plano no es síncrona.
_run_review_in_threadcorre en un hilo daemon; el bucle principal no espera a que termine antes de devolver el control al usuario. La reescritura de habilidades del turno recién revisado suele surtir efecto recién en el siguiente. - Solo se puede montar un provider de memoria externo. Honcho y Hindsight no pueden correr a la vez;
add_providerrechaza al segundo. _digest_historysolo aplica cuando la revisión se enruta a un modelo distinto. La ruta del modelo principal hace replay completo (caché caliente); cambiar ese comportamiento pasa por leerauxiliary.background_reviewen la config.- La frontera de redacción de la memoria está en «el cercado y la nota del sistema».
sanitize_contextsolo quita el cercado<memory-context>y la nota del sistema; no reescribe el cuerpo de la memoria — si la memoria cruda lleva datos sensibles, hay que tratarlos antes de escribir.
Resumen
El bucle de aprendizaje convierte «conversación» en «activo»: la revisión en segundo plano destila experiencia en habilidades y memoria de forma imperceptible, y se inyectan automáticamente en el siguiente turno. Las cuatro piezas clave: learning_graph (grafo), learning_mutations (reescritura), memory_manager (recuperación + redacción + resumen), background_review (digestión asíncrona). Esta es la base mecánica del «crecer junto al usuario» de Hermes, y la diferencia fundamental frente a «una llamada LLM sin estado».