/** * contextSizeGuard.ts — S496: extracted from agentLoop.ts * * Guard per la dimensione del contesto API: se il contesto supera 60KB * (≈15k token), prima taglia le sezioni opzionali del system prompt * (project_map, skill_hint), poi scarda i messaggi meno preziosi. * * Priorità messaggi: REAL_DATA/OBSERVATION/EPISTEMIC > recenti > vecchi. * * GAP-4: soglia abbassata da 90KB a 60KB — margine proattivo più sicuro * per evitare timeout e OOM su modelli con finestra effettiva < 128k token. * * @module contextSizeGuard */ import type { ApiMsg } from "./networkTools"; const CTX_MAX_BYTES = 60_000; // GAP-4: era 90_000 — abbassato per budget context proattivo /** * Riduce il contesto se supera la soglia, preservando i messaggi più preziosi. * Restituisce il nuovo array di messaggi (potenzialmente ridotto). */ export function applyContextSizeGuard( loopMessages: ApiMsg[], onStatus?: (msg: string) => void, ): ApiMsg[] { const _ctxSize = loopMessages.reduce((acc, m) => acc + String(m.content ?? "").length, 0); if (_ctxSize <= CTX_MAX_BYTES) return loopMessages; const _origCount = loopMessages.filter(m => m.role !== "system").length; onStatus?.(`🗜️ Contesto troppo lungo (${Math.round(_ctxSize / 1024)}KB) — comprimo…`); // Step 1 (RT-11): taglia sezioni opzionali del system prompt — preserva history let msgs = loopMessages; const _sysMsgObj = msgs.find(m => m.role === "system"); if (_sysMsgObj) { const _sysRaw = String(_sysMsgObj.content ?? ""); const _sysTrimmed = _sysRaw .replace(/\n?## project_map\b[\s\S]*?(?=\n## |\n---|\s*$)/, "") .replace(/\n?## skill_hint\b[\s\S]*?(?=\n## |\n---|\s*$)/, ""); if (_sysTrimmed.length < _sysRaw.length) { msgs = msgs.map(m => m.role === "system" ? { ...m, content: _sysTrimmed } : m); } } // Step 1.5: trim optional bracket sections (ordered by expendability) before message-level trim // U5: DOCS/PAGINE prime (rigenerabili), SAGGEZZA ACQUISITA per ultima (lezioni uniche) // Ordine: DOCS INDICIZZATI → PAGINE GIÀ LETTE → MEMORIA RILEVANTE → SAGGEZZA ACQUISITA const _OPTIONAL_CTX_SECTIONS: RegExp[] = [ /\n?\[DOCS INDICIZZATI\][\s\S]*?(?=\n\[|$)/, /\n?\[PAGINE G\u00c0 LETTE\][\s\S]*?(?=\n\[|$)/, /\n?\[MEMORIA RILEVANTE\][\s\S]*?(?=\n\[|$)/, /\n?\[SAGGEZZA ACQUISITA[^\]]*\][\s\S]*?(?=\n\[|$)/, ]; for (const _re of _OPTIONAL_CTX_SECTIONS) { const _curSize = msgs.reduce((acc, m) => acc + String(m.content ?? "").length, 0); if (_curSize <= CTX_MAX_BYTES) break; // già sotto soglia msgs = msgs.map(m => m.role === "system" ? { ...m, content: String(m.content ?? "").replace(_re, "") } : m, ); } // Step 2: se ancora sopra soglia, scarta i messaggi più vecchi (non-system) const _ctxAfterTrim = msgs.reduce((acc, m) => acc + String(m.content ?? "").length, 0); if (_ctxAfterTrim > CTX_MAX_BYTES) { const _sysMsg = msgs.filter(m => m.role === "system"); let _rest = msgs.filter(m => m.role !== "system"); // Fix D (S380): taglia prima i messaggi non-preziosi — preserva REAL_DATA/OBSERVATION/EPISTEMIC // N4: più segnali di contenuto "prezioso" da preservare nel trim const _isValuable = (m: { content?: unknown }) => /REAL_DATA|OBSERVATION|RISULTATO|\[EPISTEMIC:|PROOF|CONFIRMED|SCRAPED|\[TOOL_RESULT|\[SEARCH_RESULT|SUCCESSO_TOOL/.test(String(m.content ?? "")); const _keepCount = Math.ceil(_rest.length * 0.65); const _valuable = _rest.filter(_isValuable); const _nonVal = _rest.filter(m => !_isValuable(m)); const _nonValKeep = Math.max(0, _keepCount - _valuable.length); const _keptSet = new Set<(typeof _rest)[number]>([..._nonVal.slice(_nonVal.length - _nonValKeep), ..._valuable]); _rest = _rest.filter(m => _keptSet.has(m)); msgs = [..._sysMsg, ..._rest]; } // GAP-R1: narrazione post-trim — informa su quanti messaggi sono stati eliminati const _keptCount = msgs.filter(m => m.role !== "system").length; if (_keptCount < _origCount) { onStatus?.(`✓ Contesto compresso: ${_keptCount}/${_origCount} messaggi mantenuti (priorità: osservazioni + recenti)`); } return msgs; }