Manuale di riferimento MALDA™

Il linguaggio di programmazione AI-First - Versione 1.0.11

19. GraphMemory

GraphMemory è una classe built-in che offre capacità di memoria semantica per gli agenti AI. Combina un knowledge graph (per modellare le relazioni) con un database vettoriale (per la similarity search) così gli agenti possono ricordare fatti, comprendere le relazioni e recuperare il contesto rilevante in base alla similarità semantica.

GraphMemory è particolarmente potente quando si usa con la classe Agent: inietta automaticamente le memorie rilevanti nei prompt dell'agente, per risposte consapevoli del contesto attraverso più sessioni.

19.1 Creare un GraphMemory

Le istanze GraphMemory si creano con il costruttore new GraphMemory():

var memory = new GraphMemory();
memory.initialize();

Dopo la creazione, bisogna chiamare initialize() prima di usare gli altri metodi. Si possono specificare opzionalmente dimensione del vettore e precisione:

// Initialize with default settings (384 dimensions, single precision)
var memory = new GraphMemory();
memory.initialize();

// Initialize with custom dimension
var memory2 = new GraphMemory();
memory2.initialize(512);  // 512-dimensional vectors

// Initialize with custom dimension and precision
var memory3 = new GraphMemory();
memory3.initialize(768, "double");  // 768 dimensions, double precision

Costruttore:

Parametri di initialize:

Funzioni di embedding personalizzate

Di default GraphMemory usa embedBagOfWords per gli embedding di testo. Per una comprensione semantica migliore si può fornire una funzione di embedding personalizzata che usa embedding di reti neurali (es. da LlamaEmbedder).

Perché usare embedding neurali con GraphMemory?

Esempio:

// Use neural embeddings for better semantic understanding
var embedder = new LlamaEmbedder("path/to/embedding-model.gguf");
function neuralEmbed(text) {
    return embedder.getEmbeddings(text);
}

var memory = new GraphMemory();
memory.initialize(384, "single", neuralEmbed);  // Custom embedding function
memory.remember("I'm a software engineer");
memory.remember("I code in Python");

// Query finds semantically related memories
var results = memory.query("What is my job?", 5);
// Finds both facts because "software engineer" and "code" are semantically
// related to "job" even though the words don't match exactly

19.2 Memorizzare i fatti

Il metodo remember() memorizza i fatti nel sistema di memoria. I fatti sono indicizzati nel database vettoriale per la similarity search e memorizzati nel knowledge graph per il tracking delle relazioni.

var memory = new GraphMemory();
memory.initialize();

// Remember a simple fact
var nodeId1 = memory.remember("My name is Alice");
print(nodeId1);  // "node_0"

// Remember a fact with context
var nodeId2 = memory.remember("I prefer dark mode", "UI preferences");
print(nodeId2);  // "node_1"

// Remember structured data
var fact = dict {
    "type": "preference",
    "value": "dark mode",
    "category": "UI"
};
var nodeId3 = memory.remember(fact, "User preferences");
print(nodeId3);  // "node_2"

Parametri:

// Structured progress note (used by Ralph Wiggum and custom loops)
memory.remember(
    "Implemented snake movement",
    "validation=true; files=game.js",
    { "phase": "movement", "iteration": 2, "type": "progress", "source": "ralph" }
);

Restituisce: un node ID stringa (es. "node_0") che si può usare con findRelated()

Come funziona:

19.2.1 Collegamento semantico automatico

Dopo ogni remember(), GraphMemory cerca nell'indice vettoriale voci precedenti simili. Quando la similarità coseno è almeno 0.6, aggiunge archi bidirezionali related_to nel knowledge graph (pesati per similarità). Così si collegano le memorie conversazionali senza chiamate API extra — lo stesso pattern usato nella ricerca sulla memoria degli agenti (es. linking di note in stile Zettelkasten / A-MEM).

var memory = new GraphMemory();
memory.initialize();

var id1 = memory.remember("My name is Alice");
memory.remember("Alice is my name");   // auto-linked to id1 when similar enough

var cluster = memory.findRelated(id1);
// cluster includes both facts via graph traversal

Gli archi di struttura del codice da analyzeFile() usano un tipo di arco distinto contains; i link conversazionali usano related_to.

19.3 Interrogare la memoria

Il metodo query() esegue una similarity search per trovare memorie rilevanti. Usa la similarità vettoriale per trovare fatti simili, poi la traversata del grafo (BFS) per trovare i nodi correlati.

var memory = new GraphMemory();
memory.initialize();

// Store some facts
memory.remember("My name is Alice");
memory.remember("I prefer dark mode");
memory.remember("I work as a software engineer");
memory.remember("I like Python programming");

// Query for relevant memories
var results = memory.query("What are my preferences?");
foreach (var result in results) {
    if (result.fact != null) {
        print(result.fact);
    }
}

// Query with custom result limit
var topResults = memory.query("programming languages", 3);  // Top 3 results

Parametri:

19.3.1 Query ibrida (semantica + recente)

Passa un oggetto options come secondo o terzo argomento per combinare la similarity search con le voci strutturate più recenti. Utile quando la history della conversazione viene azzerata a ogni iterazione (es. Ralph Wiggum) ma serve comunque una continuità deterministica.

var options = {
    "recentCount": 3,
    "hybrid": true,
    "phase": "user-auth",
    "type": "progress"
};
var results = memory.query("files changed decisions", 5, options);
// Merges up to 3 recent matching entries with up to 5 semantic matches (deduplicated)

Campi di options:

query(...) restituisce un array di nodi di memoria a meno che non sia impostato grounded: true (allora un wrapper — §19.3.15). Dopo ogni query, chiama getLastQueryDiagnostics() per un oggetto di riepilogo (null prima della prima query) con campi come vectorCandidates, bm25Candidates, afterFilters, returned, droppedByLexicalMinScore, droppedByTagFilter, droppedByTypeFilter, lexicalMinScoreApplied, lexicalMinScoreMode (number | auto-weak-vector | auto-default) e embedReady.

var hits = memory.query("plugin campi", 5, {
    "hybridLexical": true,
    "lexicalMode": "bm25",
    "lexicalMinScore": "auto",
    "tags": ["bom", "plugin"],
    "tagsMode": "any",
    "diagnostics": true,
    "explain": true
});
var diag = memory.getLastQueryDiagnostics();
print(diag.returned);
print(diag.lexicalMinScoreMode);

19.3.2 Voci recenti (getRecent)

Restituisce le voci di memoria più recenti ordinate per timestamp, senza similarity search.

var recent = memory.getRecent(5);           // last 5 entries
var phaseRecent = memory.getRecent(3, "movement");  // last 3 for phase "movement"
var typed = memory.getRecent(5, "movement", "progress");  // optional type filter
var scoped = memory.getRecent(5, "", "", "chat:123");    // optional scope filter (empty phase/type = no filter)

Restituisce: un array di oggetti memoria, ciascuno contenente:

Come funziona:

  1. Scansiona i metadata di memoria memorizzati (questa non è una search semantica o vettoriale; per quella usa query())
  2. Tiene le voci che corrispondono ai filtri opzionali phase, type e scope (stringa vuota significa nessun filtro per quell'argomento)
  3. Ordina le voci rimanenti per timestamp, le più nuove per prime
  4. Restituisce fino a count oggetti memoria in quell'ordine

19.3.3 Dimenticare le voci (forget)

Rimuove un nodo di memoria per ID dal knowledge graph, dall'indice vettoriale e dallo store di metadata. Gli archi collegati sono rimossi insieme al nodo.

var nodeId = memory.remember("Outdated fact");
var removed = memory.forget(nodeId);  // true if the node existed

Per reset in batch, usa:

19.3.4 Indicizzare file di documenti (indexDocuments)

Carica i file che corrispondono a un glob pattern (come loadDocuments) e memorizza ciascun file (o chunk) come memoria semantica con type: semantic, source: file e scope configurabile (default global). I file grandi sono spezzati in chunk di default (chunkSize: 2000, overlap: 100). Il context del chunk è path#chunk-N.

var count = memory.indexDocuments("**/*.md", "./docs");
var chunked = memory.indexDocuments("**/*.md", "./docs", {
    "chunkSize": 1500,
    "overlap": 100,
    "scope": "global",
    "changedOnly": true
});
print("Indexed " + count + " chunks");

Con changedOnly: true, i file il cui metadata fileHash coincide con il contenuto attuale vengono saltati; i file cambiati sostituiscono i chunk precedenti per lo stesso filePath.

19.3.5 Statistiche della memoria (stats)

Restituisce un oggetto con timestamp nodes, edges, byType, byScope, oldest e newest (ISO 8601).

var s = memory.stats();
print(s.nodes);
print(s.byType.semantic);

19.3.5a Health check (validate)

Controlla la coerenza tra metadata, nodi del grafo, voci dell'indice vettoriale e archi. Restituisce { ok, issues, counts }. Tipi di issue: metadata_without_graph, graph_without_metadata, vector_without_node, node_without_vector, dangling_edge.

var report = memory.validate();
if (!report.ok) {
    print(report.issues.length + " issues");
}

19.3.5b Backup a rotazione su save()

Prima di sovrascrivere gli artefatti, opzionalmente copia i file attuali in {base}.backup.{yyyy-MM-dd-HHmmss}.graph.json (e i corrispondenti .metadata.json / .vectordb.bin) e fa prune dei backup più vecchi.

memory.save("~/.malda/memory/assistant", { "backup": true, "maxBackups": 5 });
// Or: MALDA_MEMORY_BACKUP=true, MALDA_MEMORY_MAX_BACKUPS=5

19.3.6 Lookup del nodo e aggiornamento in-place

Usa getNode, hasNode e update per leggere o rivedere una memoria esistente senza creare un nuovo node ID.

var nodeId = memory.remember("My name is Alice", "", { "type": "semantic" });
if (memory.hasNode(nodeId)) {
    var node = memory.getNode(nodeId);
    print(node.fact);
}
memory.update(nodeId, "My name is Bob");  // refreshes vector index; keeps context/metadata when omitted
var revised = memory.getNode(nodeId);
print(revised.fact);

19.3.7 Consolidamento episodico (consolidate)

Distilla i turni episodic recenti in un singolo nodo di riepilogo semantic con archi derived_from verso gli episodici di origine. Gli episodici elaborati sono marcati consolidated: true così possono essere pruned in seguito in sicurezza.

var result = memory.consolidate({
    "scope": "chat:123",
    "minEpisodic": 3,
    "maxEpisodic": 30
});
print(result.semanticNodesCreated);
print(result.semanticNodeId);

19.3.8 Pruning in batch (prune)

Rimuove in blocco i nodi che corrispondono. Richiede almeno un filtro in options: type, scope, phase, source o olderThanDays. L'opzionale consolidated (boolean) tiene solo i nodi con/senza consolidated: true. Restituisce il numero di nodi rimossi.

var removed = memory.prune({
    "type": "episodic",
    "olderThanDays": 30,
    "scope": "chat:123",
    "consolidated": true
});
print("Removed " + removed + " memories");

19.3.9 Limiti di dimensione della memoria (enforceLimits)

Quando il conteggio totale dei nodi supera maxNodes, rimuove le voci matchanti più vecchie (default type: episodic). Restituisce il numero di nodi rimossi.

memory.enforceLimits({ "maxNodes": 5000, "type": "episodic", "scope": "chat:123" });

19.3.10 Bundle portabili (exportBundle / importBundle)

exportBundle(path) scrive gli stessi artefatti di save(path) più un manifest {base}.bundle.json. importBundle(path) valida il manifest e gli artefatti, poi ripristina grafo, metadata e indice vettoriale (come load(path)).

var manifest = memory.exportBundle("team_memory");
memory.importBundle("team_memory");

19.3.11 Riflessione LLM (reflect)

reflect(options?) consolida i turni episodici non consolidati in fatti semantici usando un flusso di estrazione orientato all'LLM. Accetta un'iniezione opzionale per i test con facts (array di { fact, confidence, category }) per bypassare le chiamate al modello esterne.

var result = memory.reflect({
    "scope": "chat:123",
    "minEpisodic": 3,
    "maxEpisodic": 30,
    "model": "openai/gpt-4o-mini"
});
print(result.factsCreated);
print(result.episodicsMarked);

19.3.11a Riflessione in background (reflectAsync)

reflectAsync(options?) schedula lo stesso lavoro di reflect() su un thread in background e restituisce subito { scheduled, pending }. Passa savePath in options per persistere gli artefatti a riflessione completata. Se un job di reflect è già in esecuzione, restituisce pending: true.

memory.reflectAsync({
    "scope": "chat:123",
    "client": llmClient,
    "savePath": "~/.malda/memory/assistant"
});

19.3.12 Reindicizzare i documenti (reindexDocuments)

reindexDocuments(pattern, dir?, options?) è un wrapper di indexDocuments con changedOnly: true di default e restituisce contatori dettagliati:

var r = memory.reindexDocuments("**/*.md", "./docs", { "scope": "global" });
print(r.indexed);
print(r.skipped);
print(r.removed);

19.3.13 Evoluzione del retrieval

Il retrieval della memoria ora tiene traccia dei metadata importance, accessCount e lastAccessed, supporta controlli di attivazione (activation, activationDecay), diversità MMR (diversity) ed esclusione per node ID (excludeNodeIds). Gli aggiornamenti semantici possono creare archi supersedes; i nodi superseded sono declassati o filtrati durante la query.

19.3.14 Manutenzione da CLI (malda memory)

Usa la CLI per ispezionare e mantenere la memoria dell'assistente in ~/.malda/memory/assistant (si può sovrascrivere con --path).

malda memory stats --json
malda memory validate --json
malda memory reindex --dir ./kb --pattern "**/*.md"
malda memory prune --type episodic --older-than-days 30 --consolidated
malda memory reflect --scope chat:123 --min-confidence 0.75
malda memory export-bundle -o ./backup/team_memory
malda memory watch --dir ./kb --pattern "**/*.md"
malda memory download-rerank

download-rerank scarica il cross-encoder Hugging Face di default (cross-encoder/ms-marco-MiniLM-L6-v2) in ~/.malda/models/cross-encoder (model.onnx + vocab.txt). Override del repo con MALDA_CROSS_ENCODER_MODEL.

19.3.15 ASK grounded (ask / grounded.wrap)

Le citazioni dagli hit GraphMemory non sono un valore first-class di match. Fai opt-in così il risultato del retrieval è un wrapper con value, citations ({ source, id?, span? }) e sourced:

var memory = new GraphMemory();
memory.initialize();
memory.remember("Alice prefers dark mode", "", { "source": "notes.md" });
var g = memory.ask("What are Alice's preferences?", 5, { "minScore": 0 });
print(g.sourced);
print(g.citations[0].source);
print(g.value.length);

// Same wrap on the existing query path:
var also = memory.query("preferences", 5, { "minScore": 0, "grounded": true });
var manual = grounded.wrap(also.value, also.citations);

ask(query, maxResults?, options?) usa lo stesso retrieval di query, poi avvolge gli hit in un wrapper. Un query semplice senza grounded: true restituisce ancora un array. Non c'è un alias piatto grounded(). Esempio: Examples/Memory/grounded_ask.malda.

19.4 Trovare nodi correlati

Il metodo findRelated() restituisce gli oggetti memoria raggiungibili da un nodo tramite traversata del grafo (BFS sugli archi in uscita, inclusi i vicini auto-collegati related_to):

var memory = new GraphMemory();
memory.initialize();

var nodeId1 = memory.remember("My name is Alice");
memory.remember("Alice is my name");

// Find memories linked to nodeId1 (auto-linked when similar)
var related = memory.findRelated(nodeId1);
print(related.length);  // e.g. 1 — the second fact

Parametri:

Restituisce: un array di oggetti memoria correlati al nodo specificato

19.5 Gestione degli elementi di codice

GraphMemory può memorizzare e analizzare elementi di codice, ed è utile per comprendere un codebase e mappare le relazioni.

19.5.1 Aggiungere elementi di codice

Il metodo addCodeElement() memorizza elementi di codice (funzioni, classi, ecc.) nella memoria:

var memory = new GraphMemory();
memory.initialize();

var elementData = dict {
    "type": "function",
    "name": "createUser",
    "description": "Creates a new user in the database"
};
var nodeId = memory.addCodeElement("UserService.createUser", elementData);
print(nodeId);  // "code_UserService.createUser"

Parametri:

Restituisce: un node ID stringa (es. "code_UserService.createUser")

19.5.2 Trovare relazioni nel codice

Il metodo findCodeRelationships() trova tutte le relazioni (archi) collegate a un elemento di codice:

var memory = new GraphMemory();
memory.initialize();

// Add code elements
memory.addCodeElement("UserService.createUser", dict { "type": "function", "name": "createUser" });
memory.addCodeElement("UserService.deleteUser", dict { "type": "function", "name": "deleteUser" });

// Find relationships for a code element
var relationships = memory.findCodeRelationships("UserService.createUser");
foreach (var rel in relationships) {
    print("Type: " + rel.type);
    if (rel.target != null) {
        print("Target: " + rel.target);
    }
    if (rel.source != null) {
        print("Source: " + rel.source);
    }
}

Parametri:

Restituisce: un array di oggetti relazione, ciascuno contenente:

19.5.3 Analizzare i file

Il metodo analyzeFile() estrae automaticamente la struttura del codice da un file sorgente MALDA e la memorizza:

var memory = new GraphMemory();
memory.initialize();

// Analyze a MALDA source file
var elementCount = memory.analyzeFile("path/to/file.malda");
print("Found " + elementCount + " code elements");

// The method extracts:
// - Classes and their relationships
// - Functions and their signatures
// - File structure and containment relationships

Parametri:

Restituisce: un conteggio integer degli elementi di codice estratti e memorizzati

Cosa estrae:

19.6 Persistenza

GraphMemory supporta il salvataggio e il caricamento dello stato della memoria su disco, per la persistenza tra sessioni.

19.6.1 Salvare la memoria

Il metodo save() salva l'intero stato della memoria su disco:

var memory = new GraphMemory();
memory.initialize();
memory.remember("Important fact");

// Save to disk (creates three files)
memory.save("my_memory");

// Creates:
// - my_memory.graph.json (knowledge graph)
// - my_memory.vectordb.bin (vector database)
// - my_memory.metadata.json (node metadata)

Parametri:

File creati:

19.6.2 Caricare la memoria

Il metodo load() ripristina uno stato di memoria salvato in precedenza: grafo (con gli archi), indice vettoriale e metadata dei nodi. Il calcolatore di embedding viene re-inizializzato così query() e le future chiamate remember() funzionano dopo il reload.

var memory = new GraphMemory();

// Load from disk (automatically initializes if needed)
memory.load("my_memory");

// Memory is restored — facts, graph links, and vector index
var results = memory.query("previous facts");
var linked = memory.findRelated("node_0");  // graph edges persist across save/load

Parametri:

Nota: chiama initialize(dimension, precision, embeddingFunction) prima di load() quando usi un embedder personalizzato; load() preserva la funzione di embedding attraverso la propria re-inizializzazione interna. L'assistente personale di default usa embedHash (384 dimensioni) e supporta i valori MALDA_MEMORY_EMBED o config.agents.memory.embed hash, bow o llama (vedi 32. Assistente personale e CLI).

19.7 Export e import del grafo

GraphMemory fornisce metodi per esportare e importare il knowledge graph separatamente dal sistema di persistenza completo.

19.7.1 Esportare il grafo

var memory = new GraphMemory();
memory.initialize();
memory.remember("Test fact");

// Export graph as JSON string
var graphJson = memory.exportGraph();
print(graphJson);

// Can be used for:
// - Sharing knowledge graphs between systems
// - Backup and restore
// - Graph analysis in external tools

Restituisce: una rappresentazione JSON stringa del knowledge graph

19.7.2 Importare il grafo

var memory = new GraphMemory();
memory.initialize();

// Import graph from JSON string
memory.importGraph(graphJson);

// Graph structure is now restored
// Note: Vector database and metadata are not restored by importGraph()

Parametri:

Nota: importGraph() ripristina solo la struttura del grafo, non il database vettoriale né i metadata. Usa load() per un ripristino completo.

19.8 Svuotare la memoria

Il metodo clear() rimuove tutte le memorie memorizzate e resetta il sistema di memoria:

var memory = new GraphMemory();
memory.initialize();
memory.remember("Some fact");

// Clear all memories
memory.clear();

// Memory is now empty (but still initialized)
// You can start storing new facts immediately

Nota: dopo lo svuotamento, la memoria è ancora inizializzata. Non serve richiamare initialize() a meno che non si vogliano cambiare dimensione o precisione.

19.9 Integrazione con gli agenti

GraphMemory è pensato per funzionare in modo fluido con la classe Agent. Quando la memoria è abilitata su un agente, automaticamente:

var client = new OpenRouterClient();
var agent = new Agent("Assistant", "helper", "You help users.", client);

// Enable memory
agent.enableMemory();

// Agent can now remember facts
agent.remember("User prefers dark mode");

// Memory is automatically queried and injected into prompts
var response = agent.think("What are my preferences?");
// Agent will have access to "User prefers dark mode" in its context

// Access memory directly if needed
var memory = agent.getMemory();
if (memory != null) {
    var results = memory.query("preferences");
    // Process results...
}

Metodi di memoria dell'agente:

19.9.1 Tool di memoria del progresso

Dopo addMemoryProgressTools(), l'LLM può leggere e scrivere note di progresso in modo intenzionale durante un turno:

agent.enableMemory();
agent.addMemoryProgressTools();
// LLM can call remember_progress / recall_progress during tool rounds

19.10 Memoria condivisa

Più agenti possono condividere la stessa istanza GraphMemory così vedono i fatti memorizzati gli uni dagli altri. Utile per workflow multi-agente, assistenti di team, o quando gli agenti girano in actor diversi e serve una knowledge base comune.

Pattern: crea un GraphMemory, opzionalmente caricalo da disco, poi attaccalo a ciascun agente con useMemory(memory).

// Create a single memory (optionally load from disk)
var memory = new GraphMemory();
memory.initialize();
memory.load("shared_memory");   // optional: load previous state

// Create agents and attach the same memory
var agent1 = new Agent("A1", "role1", "instructions", client);
var agent2 = new CodingAgent("A2", "role2", "instructions", client);
agent1.useMemory(memory);
agent2.useMemory(memory);

// Both agents now share the same memory
agent1.think("Remember: user prefers dark mode");
agent2.think("What are the user's preferences?");  // can see the remembered fact

// Persist when done
memory.save("shared_memory");

Persistenza: usa memory.load(path) e memory.save(path) per persistere la memoria condivisa tra le sessioni. Tutti gli agenti che usano quell'istanza vedranno gli stessi fatti memorizzati dopo il load.

Thread safety: GraphMemory è thread-safe. Quando gli agenti che usano la stessa memoria girano in actor (o thread) diversi, tutte le operazioni (query, remember, save, load, ecc.) sono serializzate per istanza, quindi la memoria condivisa si può usare in sicurezza da più actor.

19.11 Esempio completo

// Create and initialize memory
var memory = new GraphMemory();
memory.initialize(384, "single");

// Store some facts
var node1 = memory.remember("My name is Alice");
var node2 = memory.remember("I work as a software engineer");
var node3 = memory.remember("I prefer dark mode", "UI preferences");
var node4 = memory.remember("I like Python and JavaScript", "programming languages");

// Query for relevant memories
var results = memory.query("What programming languages do I like?");
print("Found " + results.length + " relevant memories");
foreach (var result in results) {
    print("- " + result.fact);
}

// Find related nodes
var related = memory.findRelated(node1);
print("Found " + related.length + " related nodes");

// Add code elements
memory.addCodeElement("UserService.createUser", dict {
    "type": "function",
    "name": "createUser",
    "description": "Creates a new user"
});

// Analyze a source file
var count = memory.analyzeFile("src/UserService.malda");
print("Extracted " + count + " code elements");

// Save memory for later
memory.save("my_agent_memory");

// Later, load it back
var memory2 = new GraphMemory();
memory2.load("my_agent_memory");

// Query still works
var results2 = memory2.query("user creation");
print("Found " + results2.length + " results after loading");

19.12 Casi d'uso

GraphMemory è ideale per:

19.13 Dettagli tecnici