Spaces:
Sleeping
Sleeping
| // 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; | |
| } | |