Saltar a contenido

DDR-004 — Almacenamiento portable (SQLite como backend por defecto)

Versión: 0.1.0 | Fecha: 2026-05-09 | Estado: Aceptado (merge 2026-05-10, PR #5)

Contexto

A través de DDR-001 → DDR-003, Engrama consolidó una arquitectura limpia de cuatro capas: herramientas → skills → engine → protocolos → backends. Los protocolos existían (GraphStore, VectorStore, EmbeddingProvider) pero solo se distribuía una implementación de cada uno: la pila Neo4j.

Eso significaba que todo camino de "probar Engrama" pasaba por:

  1. Instalar Docker Desktop.
  2. Descargar una imagen de Neo4j de ~500 MB.
  3. Crear credenciales, rellenar .env.
  4. Ejecutar docker compose up, esperar a que la JVM arranque.
  5. Ejecutar engrama init para aplicar las restricciones de Cypher.

Para alguien evaluando un framework de memoria para agentes en un portátil, eso es un prerrequisito con múltiples pasos. Para alguien que distribuye una CLI o una biblioteca que embebe Engrama como capa de memoria, obligar a los usuarios a tener Docker es inviable.

En paralelo, apareció un problema más amplio: la capa EmbeddingProvider solo tenía una implementación en producción (Ollama). Quien quisiera usar OpenAI, Jina, LM Studio, vLLM o llama.cpp tenía que escribir un nuevo proveedor, aunque todos esos servicios ya hablan el mismo protocolo HTTP (la API de embeddings de OpenAI).

Decisión

Hacer que Engrama sea instalable y útil con cero servicios externos, distribuyendo un backend basado en SQLite como valor por defecto y consolidando los proveedores de embedding detrás de un único cliente compatible con OpenAI.

Concretamente

  1. Backend por defecto = SQLite + sqlite-vec.
  2. GRAPH_BACKEND por defecto es sqlite (antes era neo4j).
  3. El almacenamiento reside en ~/.engrama/engrama.db (modificable vía ENGRAMA_DB_PATH).
  4. Los datos del grafo usan las tablas relacionales de SQLite; la búsqueda de texto completo usa FTS5; la búsqueda vectorial usa la tabla virtual vec0 de la extensión sqlite-vec. Los tres viven en el mismo archivo.

  5. Neo4j pasa a ser un extra opcional.

  6. Se instala con uv sync --extra neo4j (o pip install engrama[neo4j] cuando Engrama esté en PyPI).
  7. El driver de neo4j deja de ser una dependencia base.
  8. El comportamiento no cambia cuando GRAPH_BACKEND=neo4j y las credenciales están en .env.

  9. Proveedor único de embeddings compatible con OpenAI.

  10. engrama/embeddings/openai_compat.py cubre OpenAI, Ollama (/v1/embeddings), LM Studio, vLLM, llama.cpp, Jina y cualquier futuro servicio que hable el mismo formato.
  11. El antiguo OllamaProvider se mantiene como wrapper de conveniencia para usuarios que prefieran las variables de entorno anteriores.

  12. La factoría unifica el cableado en toda la pila.

  13. engrama/backends/__init__.py expone create_stores() y create_async_stores().
  14. La CLI, el SDK y el servidor MCP despachan todos a través de la factoría.
  15. Cambiar de backend es un cambio de una sola variable.

  16. El modelo de datos es compartido.

  17. Mismas etiquetas, mismas relaciones, misma clasificación facetada.
  18. SQLite codifica las etiquetas en una columna label y las relaciones en una tabla edges; Neo4j usa etiquetas de nodo y aristas nativas.
  19. Las suites de tests de contrato (tests/contracts/) parametrizan sobre ambos backends y sobre almacenes síncronos y asíncronos, de modo que el contrato a nivel de interfaz se cumple.

Consecuencias

Positivas

  • El tiempo de onboarding baja a segundos. git clone + uv syncuv run engrama verifyuv run engrama search foo. Sin Docker, sin JVM. (Engrama aún no está en PyPI; una vez publicado, el camino se reduce a pip install engrama && engrama verify.)
  • La CI / los tests funcionan en cualquier sitio. No hay servicio que arrancar. La suite completa de tests de SQLite (~76 tests) pasa contra un checkout vacío sin .env.
  • El uso embebido se vuelve viable. Engrama puede distribuirse dentro de otra CLI, una aplicación de escritorio o un runtime en el edge sin arrastrar Docker a la cadena de dependencias.
  • Más opciones de embedder gratis. El cliente compatible con OpenAI desbloquea LM Studio, vLLM, llama.cpp, Jina, OpenAI propiamente dicho y Ollama a través del mismo código.
  • La historia de portabilidad del vault se refuerza. Combinado con DDR-002 (cada relación se persiste en el frontmatter del vault), una instalación limpia de SQLite apuntando a un vault de Obsidian existente reconstruye el grafo completo ejecutando engrama_sync_vault.

Negativas

  • Techo de escritor único en SQLite. El modo WAL gestiona bien los lectores concurrentes, pero solo un proceso puede escribir a la vez. Las configuraciones multi-agente deberían elegir Neo4j.
  • Sin Cypher ad-hoc en SQLite. La detección de patrones en reflect requirió traducir cada consulta Cypher a SQL; los puntos de entrada run_pattern / run_cypher lanzan NotImplementedError en SQLite.
  • La búsqueda vectorial escala de forma diferente. sqlite-vec es fuerza bruta en esta fase; funciona cómodamente hasta ~100k vectores. Por encima de eso, el índice HNSW de Neo4j es la herramienta adecuada.
  • Dos backends que mantener. La suite de contrato es la mitigación: cualquier divergencia entre backends se detecta en CI. Tres errores que se colaron antes del merge de pruebas están ahora permanentemente protegidos por los tests parametrizados.

Riesgos (mitigados durante la implementación)

El PR que incorporó este cambio encontró y corrigió tres errores que merece la pena registrar, tanto como historial como para servir de barreras de protección en el trabajo futuro:

  1. Deriva del contrato del almacén asíncrono. SqliteAsyncStore reenviaba los resultados síncronos sin modificar vía __getattr__, lo que filtraba la forma legacy [{"n": ...}] al servidor MCP (que esperaba la forma enriquecida {"node": ..., "created": ...} que devuelve Neo4jAsyncStore). Corrección: delegación explícita método a método con traducción de forma; tests/contracts/test_async_graphstore_contract.py parametriza sobre ambos backends asíncronos.
  2. Reflect sobreescribía Insights aprobados. engrama_reflect llamaba a merge_node con status="pending" en cada patrón detectado, deshaciendo silenciosamente las aprobaciones del usuario. Corrección: un método get_approved_titles en cada capa del almacén más un filtro dismissed | approved en la herramienta reflect.
  3. La búsqueda descartaba el enriquecimiento en resultados puramente semánticos. El puntuador híbrido solo copiaba summary / tags de los resultados de texto completo; los nodos clasificados únicamente por similitud vectorial aparecían vacíos. Corrección: search_similar ahora proyecta summary, tags, confidence y updated_at en ambos backends, y el puntuador los copia también en el camino vectorial.

Sustituye

Este DDR sustituye la decisión implícita en DDR-003 de que Neo4j era el único backend de producción. La capa de protocolos descrita en la Fase A de DDR-003 no cambia — DDR-004 es lo que convierte una implementación no-Neo4j en un miembro de primera clase de esa capa.

Seguimientos abiertos (no bloqueantes)

  • Saneamiento de consultas FTS5 en SQLite. El tokenizador por defecto trata - como un operador, por lo que consultas como engrama-mcp-server no pasan por la ruta de texto completo. Envolver la consulta en "…" o escapar los guiones cerraría la brecha. La forma de los resultados es correcta; solo varía el ranking. Se gestiona por separado de este DDR.
  • Herramienta de exportación/importación de primera clase. La migración entre backends hoy es un script manual del SDK. Un comando engrama export / engrama import haría simétrica la historia de migración.
  • Matriz de embedders en el README. La lista de proveedores (OpenAI / Ollama / LM Studio / vLLM / llama.cpp / Jina) merece un ejemplo práctico para cada uno.

Referencias

  • PR #5 — feat: portable storage — SQLite + sqlite-vec default backend
  • docs/portable-storage-spec.md — especificación previa a la implementación (local, en gitignore).
  • backends.md — guía de decisión pública para recién llegados.
  • architecture.md — diagrama de capas actualizado.