feat: supersede di memorie false (qmem_correct, include_superseded, playbook sez. 11)

- gateway: supersedes_id validato (404/409), backlink superseded_by+superseded_at+supersede_reason sul vecchio record, indici keyword, search esclude i superseduti di default (include_superseded per lineage), risposta con lineage
- estensione: tool qmem_correct (memory_id o query), qmem_store con supersedes_id/supersede_reason, qmem_search con include_superseded
- README/playbook aggiornati, versione 1.1.0
This commit is contained in:
enne2
2026-08-13 12:41:14 +02:00
parent eadd2a756b
commit 1713e68b82
5 changed files with 513 additions and 12 deletions
+113 -2
View File
@@ -142,6 +142,8 @@ export default function qmemExtension(pi: ExtensionAPI) {
),
source: Type.Optional(Type.String({ description: "Origine del record (es. conversazione, file, ticket)." })),
expires_at: Type.Optional(Type.String({ description: "Scadenza ISO 8601 (es. 2026-09-01T00:00:00Z) per memoria volatile." })),
supersedes_id: Type.Optional(Type.String({ description: "UUID del record da supersedere (correzione): il nuovo record diventa la versione attiva, il vecchio resta in archivio marcato superseded." })),
supersede_reason: Type.Optional(Type.String({ description: "Motivo della correzione (visibile in audit e sul vecchio record)." })),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
const cfg = loadConfig();
@@ -165,6 +167,8 @@ export default function qmemExtension(pi: ExtensionAPI) {
scope: p.scope ?? "agent",
source: p.source,
expires_at: p.expires_at,
supersedes_id: p.supersedes_id,
supersede_reason: p.supersede_reason,
},
signal,
);
@@ -175,7 +179,7 @@ export default function qmemExtension(pi: ExtensionAPI) {
};
}
return {
content: [{ type: "text", text: `Memoria salvata: ${data.memory_id} (${p.kind ?? "fact"}, scope ${p.scope ?? "agent"})` }],
content: [{ type: "text", text: `Memoria salvata: ${data.memory_id} (${p.kind ?? "fact"}, scope ${p.scope ?? "agent"})${p.supersedes_id ? ` — supersede ${p.supersedes_id}` : ""}` }],
details: { memory_id: data.memory_id, created_at: data.created_at },
};
},
@@ -207,6 +211,7 @@ export default function qmemExtension(pi: ExtensionAPI) {
description: "Filtra per scope di visibilità.",
}),
),
include_superseded: Type.Optional(Type.Boolean({ description: "Includi anche i record già superseduti/corretti (default: false)." })),
top_k: Type.Optional(Type.Integer({ description: "Numero massimo di risultati (default: 5, max 20)." })),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
@@ -228,6 +233,7 @@ export default function qmemExtension(pi: ExtensionAPI) {
kind: p.kind,
project_id: p.project_id,
scope: p.scope,
include_superseded: p.include_superseded ?? false,
top_k: p.top_k ?? 5,
},
signal,
@@ -247,7 +253,7 @@ export default function qmemExtension(pi: ExtensionAPI) {
}
const lines = results.map(
(r: any, i: number) =>
`${i + 1}. [${r.kind}/${r.scope} score=${r.score}] ${r.text}\n (id: ${r.memory_id}, agente: ${r.agent_id ?? "?"}, creato: ${r.created_at ?? "?"}${r.source ? `, fonte: ${r.source}` : ""})`,
`${i + 1}. [${r.kind}/${r.scope} score=${r.score}] ${r.text}\n (id: ${r.memory_id}, agente: ${r.agent_id ?? "?"}, creato: ${r.created_at ?? "?"}${r.source ? `, fonte: ${r.source}` : ""}${r.supersedes_id ? `, supersede ${r.supersedes_id}` : ""}${r.superseded_by ? `, ⚠️ superseduto da ${r.superseded_by}` : ""})`,
);
return {
content: [{ type: "text", text: lines.join("\n") }],
@@ -256,6 +262,111 @@ export default function qmemExtension(pi: ExtensionAPI) {
},
});
// =========================================================================
// TOOL: qmem_correct — supersede di una memoria falsa o superata
// =========================================================================
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.",
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)." })),
agent_id: Type.Optional(Type.String({ description: "Nome dell'agente che corregge (solo provenienza)." })),
}),
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" },
};
}
memoryId = results[0].memory_id;
orig = results[0];
}
onUpdate?.({ content: [{ type: "text", text: `qmem: supersede di ${memoryId}...` }] });
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",
source: "qmem_correct",
supersedes_id: memoryId,
supersede_reason: p.reason,
},
signal,
);
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.`,
},
],
details: { new_id: data.memory_id, superseded_id: memoryId },
};
},
});
// =========================================================================
// COMANDO: /qmem:config — menu interattivo + modalità CLI rapida
// =========================================================================