import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import { gatewayRequest, loadConfig } from "../shared"; export function registerQmemCorrect(pi: ExtensionAPI) { pi.registerTool({ name: "qmem_correct", label: "Qmem memory correct", description: "Corregge una memoria falsa o superata: crea un NUOVO record che supersede il vecchio (che resta " + "in archivio marcato superseded, mai eliminato). Passa memory_id se lo conosci (dalla risposta di " + "qmem_search), oppure query per individuare automaticamente il record attivo più rilevante. Il testo " + "corretto sostituisce quello vecchio nella ricerca semantica. Usalo quando hai evidenza verificata che " + "una memoria è falsa: contraddizione con fonte autorevole, conferma dell'utente o esito di un'azione. " + "Con query: rifiuta se lo score del top-1 è sotto la soglia (correctMinScore, default 0.60) per evitare " + "di supersedere il record sbagliato — in quel caso verifica e riprova con memory_id esplicito.", promptGuidelines: [ "qmem_correct: correggi solo con evidenza verificata (fonte autorevole, conferma utente, esito di azione) — mai per semplice dubbio o opinione.", "qmem_correct: il vecchio record resta in archivio marcato superseded — mai eliminare (tranne duplicati esatti).", ], parameters: Type.Object({ memory_id: Type.Optional(Type.String({ description: "UUID del record attivo da supersedere (dalla risposta di qmem_search)." })), query: Type.Optional(Type.String({ description: "Query per trovare il record da correggere (usata solo se memory_id non è fornito)." })), corrected_text: Type.String({ description: "Il testo corretto e verificato che sostituisce quello falso." }), reason: Type.Optional(Type.String({ description: "Motivo della correzione (visibile in audit e sul vecchio record)." })), kind: Type.Optional( Type.Union( [Type.Literal("decision"), Type.Literal("fact"), Type.Literal("episode"), Type.Literal("preference")], { description: "Tipo del nuovo record (default: eredita dal record superseduto)." }, ), ), project_id: Type.Optional(Type.String({ description: "Progetto del nuovo record (default: eredita dal record superseduto)." })), confidence: Type.Optional( Type.Union([Type.Literal("high"), Type.Literal("medium"), Type.Literal("low")], { description: "Affidabilità del nuovo record (default: eredita dal record superseduto).", }), ), agent_id: Type.Optional(Type.String({ description: "Nome dell'agente che corregge (solo provenienza)." })), parent_id: Type.Optional(Type.String({ description: "UUID del record genitore (default: eredita dal record superseduto se presente)." })), level: Type.Optional( Type.Union([Type.Literal("L1_ROOT"), Type.Literal("L2_SUBTOPIC"), Type.Literal("L3_DETAIL")], { description: "Livello gerarchico del nuovo record.", }), ), topic: Type.Optional(Type.String({ description: "Topic ID gerarchico del nuovo record." })), links: Type.Optional( Type.Array( Type.Object({ target_id: Type.String(), predicate: Type.Optional(Type.String()), weight: Type.Optional(Type.Number()), }), { description: "Collegamenti relazionali espliciti." }, ), ), }), 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.query) { return { content: [{ type: "text", text: "Serve memory_id (da qmem_search) oppure query per trovare il record da correggere." }], details: { error: "missing_target" }, }; } let memoryId = p.memory_id; let orig: any = {}; if (!memoryId) { onUpdate?.({ content: [{ type: "text", text: `qmem: ricerca del record da correggere ("${p.query}")...` }] }); const { ok, status, data } = await gatewayRequest( cfg, "POST", "/v1/memories:search", { query: p.query, top_k: 1, include_superseded: false }, signal, ); if (!ok) { return { content: [{ type: "text", text: `Errore ${status}: ${JSON.stringify(data)}` }], details: { error: "gateway_error", status }, }; } const results = data.results ?? []; if (results.length === 0) { return { content: [{ type: "text", text: "Nessun record attivo trovato per la query. Nessuna correzione applicata." }], details: { error: "not_found" }, }; } // Soglia di sicurezza: rifiuta correzioni su record deboli/rumorosi const minScore = cfg.correctMinScore ?? 0.6; if (results[0].score < minScore) { return { content: [ { type: "text", text: `Correzione rifiutata: il record più rilevante ha score ${results[0].score} < soglia ${minScore} (evidenza debole/rumorosa). ` + `Verifica il record con qmem_search (include_superseded=true per la lineage) e riprova con memory_id esplicito, oppure abbassa correctMinScore nella config.`, }, ], details: { error: "low_score", score: results[0].score, min_score: minScore }, }; } memoryId = results[0].memory_id; orig = results[0]; } else { // memory_id fornito: recupera il record per ereditare kind/scope/project_id const { ok, status, data } = await gatewayRequest(cfg, "GET", `/v1/memories/${memoryId}`, undefined, signal); if (!ok) { return { content: [{ type: "text", text: `Errore ${status}: ${JSON.stringify(data)}` }], details: { error: "gateway_error", status }, }; } orig = data; } onUpdate?.({ content: [{ type: "text", text: `qmem: supersede di ${memoryId}...` }] }); // Idempotency: stessa key per tutta l'operazione (e per eventuali retry) const idemKey = crypto.randomUUID(); const { ok, status, data } = await gatewayRequest( cfg, "POST", "/v1/memories", { text: p.corrected_text, kind: p.kind ?? orig.kind ?? "fact", agent_id: p.agent_id, project_id: p.project_id ?? orig.project_id, scope: orig.scope ?? "agent", confidence: p.confidence ?? orig.confidence ?? "medium", source: "qmem_correct", supersedes_id: memoryId, supersede_reason: p.reason, parent_id: p.parent_id ?? orig.parent_id, level: p.level ?? orig.level, topic: p.topic ?? orig.topic, links: p.links ?? orig.links, }, signal, idemKey, ); if (!ok) { return { content: [{ type: "text", text: `Errore ${status}: ${JSON.stringify(data)}` }], details: { error: "gateway_error", status }, }; } return { content: [ { type: "text", text: `Correzione applicata: nuovo record ${data.memory_id} supersede ${memoryId}${p.reason ? ` (motivo: ${p.reason})` : ""}. Il vecchio record resta in archivio marcato superseded.${data.reparented ? ` ${data.reparented} figli ri-parentati al nuovo UUID.` : ""}`, }, ], details: { new_id: data.memory_id, superseded_id: memoryId, reparented: data.reparented ?? 0 }, }; }, }); }