Skip to content

Knowledge Layers

Acervo separates knowledge into two layers. This distinction is fundamental to how the memory graph works.

graph TD
    subgraph PERSONAL["PERSONAL layer (user-specific)"]
        sandy["Sandy (Persona)"]
        avs["Altovallestudio (Organizacion)"]
        butaco["Butaco (Proyecto)"]
    end
    subgraph UNIVERSAL["UNIVERSAL layer (world knowledge)"]
        cipo["Cipolletti (Lugar)"]
        rn["Rio Negro (Lugar)"]
        angular["Angular (Tecnologia)"]
    end

    sandy -->|TRABAJA_EN| avs
    sandy -->|VIVE_EN| cipo
    cipo -->|ubicado_en| rn
    sandy -->|DUEÑO_DE| butaco
    butaco -->|USA_TECNOLOGIA| angular

    classDef personal fill:#3a1f5c,stroke:#7c3aed
    classDef universal fill:#1a3a5c,stroke:#3a7abd
    class sandy,avs,butaco personal
    class cipo,rn,angular universal

Purple = PERSONAL · Blue = UNIVERSAL · Edges can cross layers


Layer 1 — Universal (UNIVERSAL)

Verifiable world knowledge. Cities, countries, programming languages, frameworks, historical facts.

  • Source: "world" — externally verifiable
  • Immutable: once verified, it doesn't change per user
  • Examples: "Cipolletti is in Rio Negro", "React is a JavaScript framework"
from acervo import TopicGraph
from acervo.layers import Layer

graph = TopicGraph()

# A universal fact — anyone can verify this
graph.upsert_entities(
    entities=[("Cipolletti", "Lugar"), ("Rio Negro", "Lugar")],
    relations=[("Cipolletti", "Rio Negro", "ubicado_en")],
    facts=[("Cipolletti", "Ciudad en la provincia de Rio Negro, Argentina", "world")],
    layer=Layer.UNIVERSAL,
    source="world",
)

Node structure (Layer 1)

{
  "id": "cipolletti",
  "label": "Cipolletti",
  "type": "Lugar",
  "layer": "UNIVERSAL",
  "source": "world",
  "confidence_for_owner": 1.0,
  "status": "complete",
  "pending_fields": [],
  "facts": [
    {
      "fact": "Ciudad en la provincia de Rio Negro, Argentina",
      "source": "world"
    }
  ]
}

Planned: Community knowledge packs

Pre-built UNIVERSAL layer graphs for specific domains (programming languages, geography, etc.) are planned for a future release. These will be installable and shareable. See Roadmap.


Layer 2 — Personal (PERSONAL)

User-specific context. Projects, teams, preferences, work history. Things the user tells the agent about themselves.

  • Source: "user_assertion" — the user said so
  • Not shareable: belongs to one user's context
  • Mutable: the user can correct or update facts
  • Trusted within context: treated as ground truth for that user, like a new employee trusting what their manager says
  • Examples: "Sandy works at Altovallestudio", "Butaco uses Angular"
from acervo import TopicGraph
from acervo.layers import Layer

graph = TopicGraph()

# A personal fact — Sandy says so, real for Sandy
graph.upsert_entities(
    entities=[("Sandy", "Persona"), ("Altovallestudio", "Organizacion")],
    relations=[("Sandy", "Altovallestudio", "TRABAJA_EN")],
    facts=[("Sandy", "Sandy es dueña de Altovallestudio", "user")],
    layer=Layer.PERSONAL,
    source="user_assertion",
    confidence=1.0,
)

Node structure (Layer 2)

{
  "id": "sandy",
  "label": "Sandy",
  "type": "Persona",
  "layer": "PERSONAL",
  "source": "user_assertion",
  "confidence_for_owner": 1.0,
  "status": "complete",
  "pending_fields": [],
  "facts": [
    {
      "fact": "Sandy es dueña de Altovallestudio",
      "source": "user"
    }
  ]
}

Layers connect naturally through edges. Sandy's project (Layer 2) uses Angular (Layer 1). The agent understands both the personal context and the technical depth behind it.

# Sandy's project (PERSONAL) uses Angular (UNIVERSAL)
graph.upsert_entities(
    entities=[("Butaco", "Proyecto")],
    facts=[("Butaco", "ERP para empresa cliente, usa Angular monorepo", "user")],
    layer=Layer.PERSONAL,
)

# The relation links across layers
graph.upsert_entities(
    entities=[("Angular", "Tecnologia")],
    relations=[("Butaco", "Angular", "USA_TECNOLOGIA")],
    layer=Layer.UNIVERSAL,
    source="world",
)

Incomplete nodes

When the extractor encounters an entity it can't fully classify, it stores it as incomplete with a pending_fields list. The agent fills in the missing information naturally in future turns — without interrogating the user.

{
  "id": "unknown_project",
  "label": "ProjectX",
  "type": "Desconocido",
  "status": "incomplete",
  "pending_fields": ["tipo"],
  "facts": []
}

Once the type is resolved, the node transitions to "complete".


Node status lifecycle

Each turn, node statuses cycle to age out context:

stateDiagram-v2
    [*] --> hot: Node created or mentioned
    hot --> warm: cycle_status()
    warm --> cold: cycle_status()
    cold --> hot: User mentions entity
    warm --> hot: User mentions entity
  • hot — actively being discussed. Always included in materialized context.
  • warm — recently active. Included only if mentioned in the current message.
  • cold — inactive. Not included unless explicitly activated.

New or mentioned nodes are set to "hot". Call memory.cycle_status() at the start of each turn to age them.


Ontology

Acervo ships with built-in entity types and relations. The LLM can also create new ones on the fly.

Built-in entity types

Type Attributes
Persona nombre, edad, rol, relacion_con_owner
Personaje nombre, universo, creador
Organizacion nombre, tipo, industria, ubicacion
Proyecto nombre, stack, scaffold, arquitectura, modulos, estado
Lugar nombre, tipo, region, pais
Tecnologia nombre, tipo, version
Obra nombre, autor, fecha
Universo nombre, editorial
Editorial nombre, pais
Documento nombre, tipo, path, contenido_resumen
Regla descripcion, aplica_a, tecnologia, severity

Built-in relations

IS_A, CREATED_BY, ALIAS_OF, PART_OF, SET_IN, DEBUTED_IN, PUBLISHED_BY, TRABAJA_EN, VIVE_EN, DUEÑO_DE, PERTENECE_A, USA_TECNOLOGIA, TIENE_MODULO, GUSTA_DE, FAMILIAR_DE, RELACIONADO_CON

Auto-registration

When the LLM extracts a type or relation that doesn't exist in the ontology, Acervo registers it automatically. For example, if the LLM returns {"type": "Superhero"}, Acervo creates a new Superhero type on the fly instead of mapping it to Unknown.

Registering custom types manually

from acervo.ontology import register_type, register_relation
from acervo.layers import Layer

# Add your own entity type
register_type(
    name="Recipe",
    attributes=["ingredients", "time", "difficulty"],
    layer_default=Layer.PERSONAL,
)

# Add your own relation
register_relation("COAUTHORED_WITH")