AUDIT / src /lib /agentLoop /contextWindowManager.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 3)
95eb75a verified
Raw
History Blame Contribute Delete
6.41 kB
// S516 — estratto da agentLoop.ts: topic-boundary reset + context compression
// Gestisce la finestra di contesto prima dell'ingresso nel loop principale.
import { getTopicBoundaryNotice } from "../topicBoundary";
import type { TopicSegment } from "../topicBoundary";
import type { ApiMsg } from "./networkTools";
import { buildStructuredDigest } from "./historyCompressor"; // S-HIST: structured turn digest
// ── Jaccard similarity locale (P-CTX-SEMANTIC) ──────────────────────────────
// Self-contained, zero import aggiuntivi — sub-1ms, offline, iPhone safe.
function _tokenize(text: string): Set<string> {
return new Set(text.toLowerCase().replace(/[^a-z0-9À-ÿ]/g, ' ').split(/\s+/).filter(t => t.length > 2));
}
function _jaccardScore(query: string, text: string): number {
const a = _tokenize(query);
const b = _tokenize(text);
if (a.size === 0 || b.size === 0) return 0;
let inter = 0;
for (const t of a) if (b.has(t)) inter++;
return inter / (a.size + b.size - inter);
}
export interface ContextWindowParams {
loopMessages: ApiMsg[];
topicBoundary: "same" | "continuation" | "new_topic";
prevTopicSegment: TopicSegment | string | null | undefined;
contextKeep?: number; // default 12 — S178
/** P-CTX-SEMANTIC: query corrente per scoring semantico messaggi older. Opzionale — fallback cronologico. */
currentQuery?: string;
}
const _CONTEXT_KEEP_DEFAULT = 12; // III-a fix S178
// S380-D: protegge REAL_DATA/OBSERVATION/EPISTEMIC dal trim
const _VALUABLE_RE = /REAL_DATA|OBSERVATION|RISULTATO|EPISTEMIC|\[REGOLA\s+17\]|\[OSSERVAZIONE\]/;
// Fix 4 (S129): regex per messaggi con contesto progetto da preservare sul topic reset
const _PROJECT_RE = /\/[\w\-\.]+\.[a-z]{2,4}|[a-f0-9]{7,40}|write_file|read_file|vfs\.|package\.json|import\s+\{|const\s+\w+\s*=|function\s+\w+|class\s+\w+|src\//i;
/**
* S516: applica topic-boundary reset (S96) e/o context compression (S178/S380-D).
* Restituisce il nuovo array di loopMessages — non muta l'input.
*
* S-HIST: la sezione di compression ora usa buildStructuredDigest() invece della
* concatenazione grezza slice(0,140). Il digest strutturato per turni è semanticamente
* utile al LLM (principio "Lost in the Middle": position-1 = massima attenzione).
* Interfaccia di ritorno invariata: [sysMsg, compressedMsg, ...valuable.slice(-2), ...recent]
*/
export function applyContextWindow(p: ContextWindowParams): ApiMsg[] {
const msgs = p.loopMessages;
const keep = p.contextKeep ?? _CONTEXT_KEEP_DEFAULT;
// [S96] Topic boundary: se nuovo topic, azzera il contesto del segmento precedente.
// Mantieni solo il system prompt + un notice compatto sul topic chiuso.
if (p.topicBoundary === "new_topic" && p.prevTopicSegment) {
const _sysMsg = msgs.find(m => m.role === "system");
const _notice: ApiMsg = {
role: "system" as const,
content: getTopicBoundaryNotice(p.prevTopicSegment as TopicSegment),
};
// Fix 4 (S129): preserva fino a 4 messaggi con contesto progetto
const _projectMsgs = msgs
.filter(m => m.role !== "system" && _PROJECT_RE.test(String(m.content)))
.slice(-4);
return _sysMsg ? [_sysMsg, _notice, ..._projectMsgs] : [_notice, ..._projectMsgs];
}
// S178/S380-D/S-HIST: context compression — taglia messaggi non-valuable, digest strutturato
if (msgs.length > keep + 2) {
// S650: usa findIndex() invece di msgs[0] — consistente con il branch topic-boundary
// che già usa msgs.find(). msgs[0] assumeva che il system message fosse sempre al
// primo posto; findIndex() è defensive e funziona anche se messages vengono
// riordinati o se initialMessages non inizia con system message (caso degenere).
// Fallback a msgs[0] se nessun system message trovato (array degenere).
const _sysIdx = msgs.findIndex(m => m.role === "system");
const sysMsg = _sysIdx >= 0 ? msgs[_sysIdx] : msgs[0];
// S650: _nonSys costruisce la lista senza sysMsg → older/recent non includono sysMsg
// → nessun doppione nella finestra compressa; prima msgs.slice(1,-keep) assumeva
// sysMsg a indice 0, lasciando che sysMsg potesse comparire in recent se non a idx 0.
const _nonSys = _sysIdx >= 0 ? msgs.filter((_, i) => i !== _sysIdx) : msgs.slice(1);
// D3: filtra messaggi ephemeral (status puri) — libera 10-20% contesto su Groq
// Pattern da historyCompressor._EPHEMERAL_RE (mantenuto in sync manualmente)
const _EPHEMERAL_RECENT_RE = /^(Analisi|Ricerca|Caricamento|Sto |Processing|Elabora|Verifica|Reflection)\s.{0,50}[…\.]{1,3}$/i;
const recent = _nonSys.slice(-keep).filter(m => !_EPHEMERAL_RECENT_RE.test(String(m.content ?? "").trim()));
const older = _nonSys.slice(0, Math.max(0, _nonSys.length - keep));
const _valuable = older.filter(m => _VALUABLE_RE.test(String(m.content)));
// P-CTX-SEMANTIC: estrae i messaggi older più rilevanti semanticamente alla query corrente.
// Fallback silente: se currentQuery assente o scoring fallisce → comportamento invariato.
// Invariante: ultimi 6 messaggi di recent sempre preservati (recency bias — non toccare).
const _semanticRecalls: ApiMsg[] = (() => {
try {
const q = p.currentQuery;
if (!q || older.length === 0) return [];
const scored = older
.filter(m => !_VALUABLE_RE.test(String(m.content))) // valuable già inclusi
.map(m => ({ m, s: _jaccardScore(q, String(m.content)) }))
.filter(x => x.s > 0.05) // soglia minima per evitare rumore
.sort((a, b) => b.s - a.s)
.slice(0, 3) // top-3 più rilevanti
.map(x => x.m);
return scored;
} catch { return []; }
})();
// S-HIST: digest strutturato per turni — sostituisce concatenazione grezza slice(0,140).
// buildStructuredDigest() produce righe tipo:
// T1: "crea componente Button" → [write_file(Button.tsx)] ✅
// T2: "aggiungi dark mode" → [read_file, write_file(styles.css)] ❌
// Zero LLM calls, < 1ms, deterministic. Interfaccia di ritorno invariata.
const compressedMsg: ApiMsg = {
role: "system" as const,
content: buildStructuredDigest(older),
};
return [sysMsg, compressedMsg, ..._valuable.slice(-2), ..._semanticRecalls, ...recent];
}
return msgs;
}