/** * contextAssembler.ts — S-EXTRACT-CTX: context loading + system prompt assembly * Estratto da agentLoop.ts (righe 564–732) per ridurre la dimensione del file principale. * * Responsabilità: * 1. Carica i 5 contesti in parallelo (S733: Promise.all) * 2. Calcola budget/provider flags (S337/S524) * 3. Assembla _staticPromptPart (REMOVE-2 pattern) * 4. Espone _rebuildSys() come closure reattiva via oggetto state condiviso * 5. Mappa loopMessages con il system prompt assemblato * * Pattern "state object": le variabili mutabili mid-loop (_semanticCtx, _worldStateCtx, * _parallelSubCtx) sono proprietà di `state` — _rebuildSys() le legge da lì, così il * chiamante può aggiornarle e _rebuildSys resta reattiva senza side-effect nascosti. */ import { assembleSystemPrompt, section, SELF_HEALING_PROMPT } from "../systemPromptBuilder"; import { detectRoutingHint } from "../providers/adapters"; import { semanticSearch } from "../agent/SemanticSearch"; import { EPISTEMIC_INSTRUCTIONS } from "../epistemicTagger"; import { getUserModelContext, buildMemoryContextAsync } from "../sessionMemory"; import { suggestPattern } from "../skillPersistence"; import { projectMemory, getDecisionsContext } from "../projectMemory"; import { getKnowledgeRules, enrichSystemPrompt } from "../contextBridge"; import { getRepoMapForPrompt } from "../context/repoMap"; import { getGroqToken, getGeminiToken, getOpenRouterToken, getHFToken } from "../providerChain"; import { renderStateForPrompt } from "@/system/project-state"; import { buildContext as buildStateGraphContext } from "@/lib/projectStateGraph"; import { getLastArchGraph, buildFeatureGraph, renderFeatureGraph } from "@/lib/projectArchGraph"; import { buildMetaHint } from "../sessionAnalyzer"; import { FORMAT_DIRECTIVES } from "../formatClassifier"; import type { ApiMsg } from "./networkTools"; import { computeSystemPromptBudget } from "./systemPromptBudget"; import { loadWorldStateCtx } from "./worldStateLoader"; import { loadContextHints } from "./contextHintsLoader"; import { loadRelevantFileCtx } from "./relevantFilesLoader"; import { loadRepairAuditHint } from "./repairAuditHintLoader"; import { PLANNER_DEPS_HINT as _PLANNER_DEPS_HINT } from "./plannerDepsHint"; import { GOAL_FORMAL_INSTRUCTIONS } from "./goalFormalInstructions"; import { loadRagCtx } from "./ragCtxLoader"; import { loadSessionCtx } from "./sessionMemCtxLoader"; import { REACT_SYSTEM_SUFFIX, buildRouteAddons } from "./reactSuffix"; import { buildArchCtxCached as _buildArchCtxCached } from "./archCtxCache"; import { runWorkflowPrep } from "./workflowPrepRunner"; import { getConstraintsForPrompt } from "../agentConstraints"; // P35: negative constraints import { buildPersonaHint } from "./personaInjector"; // P44: Expertise Personas import { getTopKForWorkflow } from "./workflowRegistry"; import type { ResolvedWorkflow } from "./workflowTypes"; import { getConnectors } from "../connectors"; // GAP-1: connettori API visibili all'agente // ─── Tipi ──────────────────────────────────────────────────────────────────── export interface ContextAssemblerDeps { lastUserMsg: string; initialMessages: Array<{ role: string; content: string }>; _earlyProjectMap: string; _earlyFileCtx: string; _workflow: ResolvedWorkflow; _isCodeTask: boolean; _isHighComplexityTask: boolean; _fmt: import("../formatClassifier").OutputFormat; orchestrationHint: string; _extThinking: { hint: string; isExtended: boolean; statusLabel: string }; _knowledgeGap: { hint: string }; onStatus: (msg: string) => void; getRelevantFiles: Parameters[1]; // lazy-import values (passati dopo Promise.all in agentLoop) selfLearning: import("../selfLearning").SelfLearningEngine; adaptiveSolverHint: string; // ADAPTIVE_SOLVER_SYSTEM_HINT contextBuilder: { buildContextString: (q: string) => string }; slGetCtxCached: (msg: string) => string; // agentMemory — lazy import, opzionale (GAP ARCH-3: Dexie cache) agentMem: { list: () => unknown[] } | null; // formalGoal — letto da _rebuildSys, può essere null inizialmente getFormalGoal: () => { goal: string; doneWhen: string; outOfScope: string } | null; // executeTool — per runWorkflowPrep executeShell executeTool: (name: string, args: unknown, signal: AbortSignal) => Promise; } /** Stato mutabile mid-loop — passato per riferimento così _rebuildSys rimane reattiva */ export interface ContextState { _semanticCtx: string; _semanticCtxTs: number; _worldStateCtx: string; _parallelSubCtx: string; /** V7-3 GAP-3: Working Memory Snapshot — updated after runPlanTracker(), immune to applyContextWindow */ _workingMemCtx: string; } export interface ContextAssemblerResult { loopMessages: ApiMsg[]; /** Ricostruisce il system prompt dinamico con i valori correnti di `state` */ _rebuildSys: () => string; state: ContextState; _sessStats: { autoFixed: number; propagated: number; verifyFails: number; totalIter: number }; _repairAuditHint: string; /** C4-FIX: true se i connettori sono cambiati dall'ultima chiamata a _rebuildSys(). * Il chiamante (agentLoop/midLoopContextRefresh) può controllare questo flag per forzare * un rebuild immediato del system prompt invece di aspettare il prossimo mid-loop refresh. */ connectorsChangedSignal: { changed: boolean }; } // ─── Implementazione ───────────────────────────────────────────────────────── export async function buildLoopContext(deps: ContextAssemblerDeps): Promise { const { lastUserMsg, initialMessages, _earlyProjectMap, _earlyFileCtx, _workflow, _isCodeTask, _isHighComplexityTask, _fmt, orchestrationHint, _extThinking, _knowledgeGap, onStatus, getRelevantFiles, selfLearning, adaptiveSolverHint, contextBuilder, slGetCtxCached, agentMem, getFormalGoal, executeTool, } = deps; // 1. Calcoli sincroni — nessun await, nessuna dipendenza cross const brainCtx = (() => { // S-FIX4-9: direct import instead of fragile globalThis try { return enrichSystemPrompt(lastUserMsg); } catch { return ""; } })(); // GAP ARCH-3: legge la cache Dexie (già idratata in-memory al boot via initMemory()) const _agentMemCtx = (() => { try { if (!agentMem) return ""; type _ME = { category: string; key: string; value: string; updatedAt: number }; const _entries = (agentMem.list() as _ME[]) .sort((a, b) => b.updatedAt - a.updatedAt) .slice(0, 15); if (!_entries.length) return ""; const _lines = _entries .map(e => `- [${e.category}] ${e.key}: ${String(e.value).slice(0, 80)}`) .join('\n'); return `### Memoria agente (${_entries.length} voci — usa recall() per ricerca semantica):\n${_lines}`; } catch { return ""; } })(); const agentCtx = (() => { try { return contextBuilder.buildContextString(lastUserMsg); } catch { return ""; } })(); const _nowIso = new Date().toISOString().slice(0, 10); // YYYY-MM-DD const ROUTE_ADDONS = buildRouteAddons(_nowIso); const detectedRoute = (() => { try { return detectRoutingHint(lastUserMsg); } catch { return "reasoning"; } })(); const routeAddon = ROUTE_ADDONS[detectedRoute] ?? ""; const _baseContent = initialMessages.find(m => m.role === "system")?.content ?? ""; const _isFirstMsg = initialMessages.filter(m => m.role !== "system").length <= 1; // S99 const _personaHint = buildPersonaHint(lastUserMsg, _isCodeTask); // P44: auto-selects Coder/Architect/Researcher/Debugger // 2. Promise.all — 5 fetch asincroni in parallelo (S733) const [ projectMap, fileCtx, _semanticCtxInit, _ragCtx, _sessionCtxR, ] = await Promise.all([ // S206/Gap1-FIX: riusa cache se già fetchato (zero doppio fetch) _earlyProjectMap ? Promise.resolve(_earlyProjectMap) : (async () => { try { return await getRepoMapForPrompt(); } catch { return ""; } })(), // S212/S527: riusa cache se già fetchato (zero doppio fetch quando _needsPlan) _earlyFileCtx ? Promise.resolve(_earlyFileCtx) : loadRelevantFileCtx(lastUserMsg, getRelevantFiles, _workflow.topK ?? getTopKForWorkflow(_workflow.id)).catch(() => ""), // S167-Fix8: semantic search context semanticSearch.contextForAsync(lastUserMsg).catch((): string => ""), // S290-RAG/S532: RAG context da file allegati loadRagCtx(lastUserMsg), // S99/S380/S534: session memory + world state loadSessionCtx(lastUserMsg, _isFirstMsg, { buildMemoryContextAsync, loadWorldStateCtx }), ]); // 3. Stato mutabile mid-loop (oggetto condiviso per riferimento) const state: ContextState = { _semanticCtx: _semanticCtxInit, _semanticCtxTs: semanticSearch.cacheTs, _worldStateCtx: _sessionCtxR.worldStateCtx, _parallelSubCtx: "", _workingMemCtx: "", // V7-3 GAP-3: populated after first runPlanTracker() in agentLoop }; const _sessionMemCtx = _sessionCtxR.sessionMemCtx; // P35: vincoli negativi — async read da VFS /.agent/constraints.json const _negativeConstraintsCtx = await getConstraintsForPrompt().catch(() => ""); // 4. Context hints — Step4/7/8/S127/S526 const { strategyHint: _strategyHint, knowledgeRules: _knowledgeRules, userModelCtx: _userModelCtx, skillHint: _skillHint, recipeHint: _recipeHint } = loadContextHints(lastUserMsg, { selfLearning, getKnowledgeRules, getUserModelContext, suggestPattern }); // 5. Repair Auditor (S415b/S529) const _repairAuditHint = loadRepairAuditHint(_isCodeTask, lastUserMsg); // 6. Workflow prep — sincrono/leggero (S753) // Chiamato PRIMA di _rebuildSys → _workflowPrepCtx è già pronto alla prima invocazione const _workflowPrepCtx = await runWorkflowPrep(_workflow, { userMsg: lastUserMsg, onStatus, getRelevantFiles, topK: _workflow.topK ?? getTopKForWorkflow(_workflow.id), executeShell: async (cmd: string) => { try { const r = await Promise.race([ executeTool("execute_shell", { command: cmd }, {} as AbortSignal), new Promise(res => { setTimeout(() => res(""), 5_000); }), ]); return typeof r === "string" ? r : String(r ?? ""); } catch { return ""; } }, }).catch(() => ""); // 7. Budget per-provider (S337/S350/S524) const _hasGemini = !!getGeminiToken(); const _hasOpenRouter = !!getOpenRouterToken(); const _hasGroqOnly = !_hasGemini && !_hasOpenRouter && !!getGroqToken(); const _isSmallCtx = !getGeminiToken() && !getOpenRouterToken() && !!getGroqToken(); void _isSmallCtx; // S337-F1: alias esplicito per regression test const _hasHFOnly = !_hasGemini && !_hasOpenRouter && !getGroqToken() && !!getHFToken(); const { sysCharBudget: _sysCharBudget, baseMaxChars: _baseMaxChars, reactMaxChars: _reactMaxChars } = computeSystemPromptBudget({ hasGemini: _hasGemini, hasOpenRouter: _hasOpenRouter, hasGroqOnly: _hasGroqOnly, hasHFOnly: _hasHFOnly }, _isHighComplexityTask, _baseContent.length); // 8. Static prompt part — REMOVE-2: pre-computato una volta, ~22K chars const _staticPromptPart = assembleSystemPrompt([ { ...section("react_suffix", adaptiveSolverHint + routeAddon + REACT_SYSTEM_SUFFIX + orchestrationHint), maxChars: _reactMaxChars }, section("epistemic", EPISTEMIC_INSTRUCTIONS), section("self_healing", SELF_HEALING_PROMPT), section("goal_formal", GOAL_FORMAL_INSTRUCTIONS), section("planner_deps", _PLANNER_DEPS_HINT), ], 32_000); // 9. _rebuildSys — closure reattiva su `state` (S382/S384 Fix 3+6) const _rebuildSys = (): string => { const _formalGoal = getFormalGoal(); const _dynPart = assembleSystemPrompt([ { ...section("base", _baseContent), maxChars: _baseMaxChars }, section("project_map", projectMap), // S767: repo map early for LLM context section("brain", brainCtx + (_knowledgeRules ? "\n\n" + _knowledgeRules : "") + (state._parallelSubCtx ? "\n\n" + state._parallelSubCtx : "")), // Step7 + S731 section("procedural", _strategyHint), // Step4 section("user_model", _userModelCtx), // Step8 section("skill_hint", _skillHint), // S127 Gap 5 section("api_connectors", (() => { // GAP-1: connettori configurati visibili all'agente try { const _cs = getConnectors(); if (!_cs.length) return ""; return "### Connettori API configurati (usa connector_id= per riferirli nei tool call):\n" + _cs.map(c => `- ${c.name} (${c.baseUrl}): connector_id="${c.id}"${c.description ? " — " + c.description.slice(0, 120) : ""}` ).join("\n"); } catch { return ""; } })()), section("recipe_hint", _recipeHint), // S-RECIPE section("repair_audit", _repairAuditHint), // S415b section("agent_ctx", agentCtx), section("semantic", state._semanticCtx), // Fix 3: sostituzione completa mid-loop section("working_memory", state._workingMemCtx), // V7-3 GAP-3: immune a applyContextWindow section("rag_context", _ragCtx), // S290-RAG section("session_mem", _sessionMemCtx), section("agent_memory", _agentMemCtx), // GAP ARCH-3: Dexie memories section("negative_constraints", _negativeConstraintsCtx), // P35: vincoli negativi appresi section("world_state", state._worldStateCtx), // Fix 6: refresh mid-loop section("reality_state", (() => { try { return renderStateForPrompt(); } catch { return ""; } })()), // G1-S202 section("state_graph", (() => { try { return buildStateGraphContext(); } catch { return ""; } })()), // S405 section("arch_graph", (() => { try { return _buildArchCtxCached(); } catch { return ""; } })()), // S407+S440 section("feature_graph", (() => { try { const _ag = getLastArchGraph(); if (!_ag) return ""; return renderFeatureGraph(buildFeatureGraph(_ag.nodes)); } catch { return ""; } })()), // S453 section("file_ctx", fileCtx), section("project_memory", projectMemory.getContext()), // S194 Gap 5 section("sl_ctx", (() => { try { return slGetCtxCached(lastUserMsg); } catch { return ""; } })()), // S765-M section("arch_decisions", (() => { try { return getDecisionsContext(_formalGoal?.goal ?? ""); } catch { return ""; } })()), // S453 section("ext_thinking", _extThinking.hint), // S689 section("knowledge_gap", _knowledgeGap.hint), // S691 section("workflow_ctx", _workflowPrepCtx), // S753 { name: "meta_hint", priority: 6, maxChars: 200, required: false, content: (() => { try { return buildMetaHint(); } catch { return ""; } })() }, // S-GAP3 { name: "format_directive", priority: 9, maxChars: 300, required: false, content: FORMAT_DIRECTIVES[_fmt] }, // S316 { name: "persona", priority: 8, maxChars: 600, required: false, content: _personaHint }, // P44: Expertise Personas ], _sysCharBudget - _staticPromptPart.length); return _staticPromptPart + "\n\n" + _dynPart; }; // 10. Assembla loopMessages iniziali const _assembledSys = _rebuildSys(); const loopMessages: ApiMsg[] = initialMessages.map(m => m.role === "system" ? { ...m, content: _assembledSys } as ApiMsg : m as ApiMsg ); const _sessStats = { autoFixed: 0, propagated: 0, verifyFails: 0, totalIter: 0 }; // C4-FIX: segnale reattivo per i connettori — l'evento agent:connectors-changed // (emesso da saveConnectors()) setta changed=true, il chiamante (agentLoop) forza un rebuild. // _rebuildSys() chiama già getConnectors() lazily, quindi basta richiamarla al prossimo iter. const connectorsChangedSignal = { changed: false }; if (typeof window !== "undefined") { const _c4Handler = () => { connectorsChangedSignal.changed = true; }; window.addEventListener("agent:connectors-changed", _c4Handler); // Cleanup automatico: listener rimosso se il segnale viene usato > 1 volta e poi pulito dal chiamante // (il chiamante deve chiamare window.removeEventListener("agent:connectors-changed", _c4Handler) // se vuole pulizia esplicita — non è richiesto perché l'agentLoop dura la durata della sessione) } return { loopMessages, _rebuildSys, state, _sessStats, _repairAuditHint, connectorsChangedSignal }; }