// 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 { 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; }