UNPKG

scai

Version:

> **AI-powered CLI for local code analysis, commit message suggestions, and natural-language queries.** 100% local, private, GDPR-friendly, made in Denmark/EU with ❤️.

256 lines (255 loc) 13 kB
import { builtInModules } from "../pipeline/registry/moduleRegistry.js"; import { logInputOutput } from "../utils/promptLogHelper.js"; import { planResolverStep } from "./planResolverStep.js"; import { infoPlanGen } from "./infoPlanGenStep.js"; import { understandIntentStep } from "./understandIntentStep.js"; import { structuralAnalysisStep } from "./structuralAnalysisStep.js"; import { contextReviewStep } from "./contextReviewStep.js"; import { planTargetFilesStep } from "./planTargetFilesStep.js"; import { validationAnalysisStep } from "./validationAnalysisStep.js"; import { preFileSearchCheckStep } from "./preFileSearchCheckStep.js"; import { semanticAnalysisStep } from "./semanticAnalysisStep.js"; import { selectRelevantSourcesStep } from "./selectRelevantSourcesStep.js"; import { transformPlanGenStep } from "./transformPlanGenStep.js"; import { finalPlanGenStep } from "./finalPlanGenStep.js"; /** Build a registry of all built-in modules */ const MODULE_REGISTRY = Object.fromEntries(Object.entries(builtInModules).map(([name, mod]) => [name, mod])); /** Resolve a module from the registry */ function resolveModuleForAction(action) { const mod = MODULE_REGISTRY[action]; if (!mod) console.warn(`⚠️ Missing module for action "${action}" — skipping`); return mod; } export class MainAgent { constructor(context) { this.runCount = 0; this.maxRuns = 2; this.context = context; this.query = context.initContext?.userQuery ?? ''; } /** Generic step executor */ async executeStep(step, input) { console.log(`\n`); console.log(`\n =====================================================================================`); console.log(`\n⚡ Executing step: ${step.action}`); if (step.description) console.log(`\n --> Description: ${step.description}`); console.log(`\n =====================================================================================`); // attach the current step to the context if (input.context) { input.context.currentStep = step; // <-- new property } const mod = resolveModuleForAction(step.action); if (!mod) { return { query: input.query, content: input.content, data: { skipped: true }, context: input.context }; } const moduleInput = { query: step.description ?? input.query, content: input.data ?? input.content, context: input.context // shared reference }; try { console.log(` ▶ Running module: ${mod.name}`); const output = await mod.run(moduleInput); if (!output) throw new Error(`Module '${mod.name}' returned empty output`); // Return only what matters — context is always the same reference return { query: step.description ?? input.query, data: output.data, context: input.context }; } catch (err) { console.error(` ❌ Module "${mod?.name}" failed:`, err); return { query: input.query, content: input.content, data: { error: String(err) }, context: input.context }; } } /** Main run sequence */ async run() { var _a; this.runCount++; console.log(`\n =====================================================================================`); console.log(`🚀 ================= [AutonomousAgent] Starting run #${this.runCount} =================`); logInputOutput("GlobalContext (structured)", "input", this.context); // --------------------------------------------------------------------- // GENERATE FOLDER-CAPSULE SUMMARIES // --------------------------------------------------------------------- if (this.context.initContext?.folderCapsules?.length) { const capsulesSummary = this.context.initContext.folderCapsules.map(fc => ({ path: fc.path, fileCount: fc.stats?.fileCount ?? 0, depth: fc.depth ?? 0, confidence: fc.confidence ?? 0, roles: fc.roles ?? [], concerns: fc.concerns ?? [] })); // Add a human-readable summary for the LLM (_a = this.context).analysis ?? (_a.analysis = {}); this.context.analysis.folderCapsulesSummary = capsulesSummary; const humanReadable = capsulesSummary.map(fc => `- ${fc.path}: ${fc.fileCount} files, depth ${fc.depth}, confidence ${fc.confidence}`).join('\n'); this.context.analysis.folderCapsulesHuman = humanReadable; console.log('\n📂 Folder Capsules Summary (for agent steps):\n', humanReadable); } // --------------------------------------------------------------------- // 0️⃣ UNDERSTAND INTENT — First-pass interpretation of query // --------------------------------------------------------------------- console.log("\n🧠 Running understandIntentStep (pre-analysis fixture)\n"); await understandIntentStep.run({ context: this.context }); // --------------------------------------------------------------------- // 1️⃣ PLAN RESOLVER — Fast path (context-driven routing) // --------------------------------------------------------------------- await planResolverStep.run(this.context); // -------------------------------------------------- // 🚦 ROUTING DECISION // -------------------------------------------------- const routing = this.context.analysis?.routingDecision; // ✅ FAST PATH: if planResolver thinks we have a final answer, wrap it in ModuleIO and return if (routing?.decision === 'final-answer' && routing.answer) { console.log("⚡ Fast path hit — returning final answer directly"); return { query: this.query, data: { finalAnswer: routing.answer, source: "planResolver", }, context: this.context }; } // -------------------------------------------------- // ⏩ FALL THROUGH: continue with plan generation // -------------------------------------------------- let textPlan; // Use rationale from routing if available (temporary compatibility) if (typeof routing?.rationale === 'string' && routing.rationale.trim()) { textPlan = routing.rationale; } // Fallback: use the original query if no plan/rationale is available if (!textPlan || textPlan.trim().length === 0) { textPlan = this.query; } // textPlan can now be passed to planGeneratorStep // --------------------------------------------------------------------- // PRE-FILE-SEARCH CHECK (do we have the files required to continue ) // --------------------------------------------------------------------- await preFileSearchCheckStep(this.context); // --------------------------------------------------------------------- // 2️⃣ INFO PLAN GENERATION (information-only steps) // --------------------------------------------------------------------- await infoPlanGen.run(this.context); const infoPlan = this.context?.analysis?.planSuggestion?.plan ?? { steps: [] }; const infoSteps = infoPlan.steps.filter(s => s.groups?.includes("info")); console.log("information gathering steps:\n"); console.dir(infoSteps); let stepIO = { query: this.query, context: this.context }; // ===================================================== // INFORMATION ACQUISITION PHASE // Purpose: gather raw information, no interpretation // ===================================================== //let count = 0; for (const step of infoSteps) { //count++; stepIO = await this.executeStep(step, stepIO); /* if (count === 4) { debugContext(this.context, { step: "first step", note: "searchFiles?" }); } */ } // ===================================================== // ANALYSIS PHASE // Purpose: understand what we have and what is being asked // ===================================================== // 🎯 RELEVANT SOURCE SELECTION (authoritative narrowing) console.log("\n📁 Selecting relevant source files\n"); await selectRelevantSourcesStep.run({ query: this.query, context: this.context }); // ANALYSIS PHASE (deterministic, durable) console.log("\n🏗️ Running structural analysis\n"); await structuralAnalysisStep.run({ query: this.query, context: this.context }); // SEMANTIC ANALYSIS PHASE (LLM-driven) console.log("\n🧠 Running semantic analysis\n"); await semanticAnalysisStep.run({ query: this.query, context: this.context }); // SEMANTIC PLAN TARGET FILES (moves focus -> plan.targetFiles) console.log("\n🧠 Running plan target files\n"); await planTargetFilesStep.run({ query: this.query, context: this.context }); /* debugContext(this.context, { step: "Semantic analysis step", note: "semantic meaning?" }); */ // VALIDATION PHASE console.log("\n🧠 Running validation analysis\n"); await validationAnalysisStep.run({ query: this.query, context: this.context }); // ===================================================== // ROUTING / PLANNING PHASE // ===================================================== const review = await contextReviewStep(this.context); if (review.decision === "loopAgain" && this.runCount < this.maxRuns) { console.log("🔄 Looping for additional context"); // Clear noisy bootstrap artifacts before looping this.resetInitContextForLoop(); return this.run(); } // ===================================================== // TRANSFORM PLAN GENERATION (after context review) // Purpose: produce actionable transform steps // ===================================================== console.log("\n🛠️ Generating transform plan\n"); await transformPlanGenStep.run(this.context); // Filter transform steps const transformPlan = this.context?.analysis?.planSuggestion?.plan ?? { steps: [] }; const transformSteps = transformPlan.steps.filter(s => s.groups?.includes("transform")); console.log("transform steps:\n"); console.dir(transformSteps); // ===================================================== // TRANSFORM PHASE // Purpose: produce concrete changes or artifacts // ===================================================== console.log("\n⚡ Running transform steps\n"); for (const step of transformSteps) { stepIO = await this.executeStep(step, stepIO); } // ===================================================== // FINAL PLAN GENERATION // Purpose: produce finalize / commit steps // ===================================================== console.log("\n✅ Generating final plan\n"); await finalPlanGenStep.run(this.context); // Filter finalize steps const finalPlan = this.context?.analysis?.planSuggestion?.plan ?? { steps: [] }; const finalizeSteps = finalPlan.steps.filter(s => s.groups?.includes("finalize")); console.log("finalize steps:\n"); console.dir(finalizeSteps); // ===================================================== // FINALIZE PHASE // Purpose: commit results and respond to the user // ===================================================== for (const step of finalizeSteps) { stepIO = await this.executeStep(step, stepIO); } console.log("🏁 Agent completed."); console.log("---------------------------------------------\n"); console.log("Show final context\n", this.context); return stepIO; } // ===================================================== // Helper: reset initContext for clean loop // ===================================================== resetInitContextForLoop() { if (this.context.initContext) { this.context.initContext.relatedFiles = []; console.log(" 🧹 Cleared initContext.relatedFiles for clean loop"); } } }