AUDIT / src /lib /agentLoop /contextAssembler.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
17 kB
/**
* 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<typeof loadRelevantFileCtx>[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<string>;
}
/** 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<ContextAssemblerResult> {
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<string>(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 };
}