import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import { gatewayRequest, loadConfig } from "../shared"; export function registerQmemTree(pi: ExtensionAPI) { pi.registerTool({ name: "qmem_tree", label: "Qmem memory hierarchy tree", description: "Esplora e visualizza l'albero gerarchico di un macro-topic o di un nodo genitore (L1/L2) con tutti i suoi " + "sotto-nodi specialistici e collegamenti. Accetta memory_id (del nodo root) oppure topic " + "(es. 'ALFA-ROMEO-GT-1300-JUNIOR' o 'ALFA-ROMEO-GT-1300-JUNIOR/ROOT'). " + "Restituisce una vista ad albero gerarchico strutturata con gli UUID per una navigazione immediata.", promptGuidelines: [ "qmem_tree: usalo per avere la mappa completa di un dominio complesso prima di approfondire un ramo specialistico con qmem_get.", ], parameters: Type.Object({ memory_id: Type.Optional(Type.String({ description: "UUID del record radice (L1_ROOT) da esplorare." })), topic: Type.Optional(Type.String({ description: "Topic ID o prefisso del macro-topic (es. 'ALFA-ROMEO-GT-1300-JUNIOR')." })), }), async execute(toolCallId, params, signal, onUpdate, ctx) { const cfg = loadConfig(); if (!cfg.apiKey) { return { content: [{ type: "text", text: "Config mancante: esegui /qmem:config per impostare url e apiKey." }], details: { error: "missing_config" }, }; } const p = params as any; if (!p.memory_id && !p.topic) { return { content: [{ type: "text", text: "Fornisci almeno un memory_id o un topic da esplorare." }], details: { error: "missing_parameter" }, }; } onUpdate?.({ content: [{ type: "text", text: "qmem: recupero albero gerarchico..." }] }); let rootId = p.memory_id; let rootData: any = null; if (rootId) { const { ok, status, data } = await gatewayRequest(cfg, "GET", `/v1/memories/${rootId}`, undefined, signal); if (!ok) { return { content: [{ type: "text", text: `Errore ${status} nel recupero del nodo root: ${JSON.stringify(data)}` }], details: { error: "not_found", memory_id: rootId }, }; } rootData = data; } else { // Cerca il nodo root per topic const topicQuery = p.topic.includes("/") ? p.topic : `${p.topic}/ROOT`; const { ok, status, data } = await gatewayRequest( cfg, "POST", "/v1/memories:search", { query: topicQuery, topic: topicQuery, top_k: 1, include_superseded: false, min_score: 0.0 }, signal, ); if (!ok || !data.results || data.results.length === 0) { // Fallback: cerca con query generica const fallback = await gatewayRequest( cfg, "POST", "/v1/memories:search", { query: p.topic, level: "L1_ROOT", top_k: 1, include_superseded: false, min_score: 0.0 }, signal, ); if (!fallback.ok || !fallback.data.results || fallback.data.results.length === 0) { return { content: [{ type: "text", text: `Nessun nodo radice (L1_ROOT) trovato per il topic '${p.topic}'.` }], details: { error: "root_not_found" }, }; } rootData = fallback.data.results[0]; rootId = rootData.memory_id; } else { rootData = data.results[0]; rootId = rootData.memory_id; } } // Recupera tutti i figli associati a parent_id == rootId. // Fallback lineage: se il root è stato superseduto, i figli storici // puntano ancora al vecchio UUID (supersedes_id) — li includiamo per // garantire la navigabilità dell'albero anche per gli orfani pre-fix. const parentIds = [rootId]; if (rootData.supersedes_id) parentIds.push(rootData.supersedes_id); const children: any[] = []; const seen = new Set(); for (const pid of parentIds) { const childRes = await gatewayRequest( cfg, "POST", "/v1/memories:search", { query: "*", parent_id: pid, top_k: 20, include_superseded: false, min_score: 0.0 }, signal, ); for (const c of childRes.ok ? (childRes.data.results ?? []) : []) { if (!seen.has(c.memory_id)) { seen.add(c.memory_id); children.push(c); } } } const out: string[] = []; out.push(`🌳 ALBERO GERARCHICO: ${rootData.topic ?? "ROOT"} [${rootData.level ?? "L1_ROOT"}]`); out.push(` UUID: ${rootId} (${rootData.project_id ?? "pi-qmem"})`); if (rootData.text) { const summary = String(rootData.text).split("\n")[0].slice(0, 120); out.push(` Descrizione: ${summary}`); } out.push(""); if (children.length === 0) { out.push(" (Nessun sotto-nodo figlio L2 associato)"); } else { out.push(` └── Sotto-nodi collegati (${children.length}):`); children.forEach((c: any, idx: number) => { const isLast = idx === children.length - 1; const branch = isLast ? " └──" : " ├──"; const subTitle = c.topic ? c.topic.split("/").slice(1).join("/") : (c.kind ?? "subtopic"); const firstLine = String(c.text ?? "").split("\n")[0].slice(0, 100); out.push(`${branch} [${c.level ?? "L2"}] ${subTitle} (UUID: ${c.memory_id})`); out.push(` ${firstLine}`); }); } return { content: [{ type: "text", text: out.join("\n") }], details: { root_id: rootId, children_count: children.length }, }; }, }); }