File size: 17,027 Bytes
cc11e77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
/**
 * 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 };
}