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 ❤️.

208 lines (207 loc) 9.36 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"; import { Spinner } from "../lib/spinner.js"; /* ───────────────────────── helpers ───────────────────────── */ function startTimer() { const start = Date.now(); return () => Date.now() - start; } function logLine(phase, step, ms, desc) { // Clear current line (removes leftover spinner) process.stdout.write('\r\x1b[K'); const suffix = desc ? ` — ${desc}` : ""; const timing = typeof ms === "number" ? ` (${ms}ms)` : ""; console.log(`[AGENT] ${phase} :: ${step}${suffix}${timing}`); } /** Helper to display messages to the user from the agent */ function userOutput(message) { console.log(`[USER OUTPUT] ${message}`); } /** Helper to display messages with phase context */ function userPhaseOutput(phase, message) { console.log(`[USER OUTPUT] [${phase}] ${message}`); } /* ───────────────────────── registry ───────────────────────── */ const MODULE_REGISTRY = Object.fromEntries(Object.entries(builtInModules).map(([name, mod]) => [name, mod])); function resolveModuleForAction(action) { return MODULE_REGISTRY[action]; } /* ───────────────────────── agent ───────────────────────── */ export class MainAgent { constructor(context) { this.spinner = new Spinner(); // spinner for user feedback this.runCount = 0; this.maxRuns = 2; this.context = context; this.query = context.initContext?.userQuery ?? ""; } /* ───────────── step executor ───────────── */ async executeStep(step, input) { const stop = startTimer(); if (input.context) { input.context.currentStep = step; } const mod = resolveModuleForAction(step.action); if (!mod) { logLine("EXECUTE", step.action, stop(), "skipped (missing module)"); return { query: input.query, content: input.content, data: { skipped: true }, context: input.context }; } try { // Update spinner text for user this.spinner.update(`Running step: ${step.action}`); const output = await mod.run({ query: step.description ?? input.query, content: input.data ?? input.content, context: input.context }); if (!output) { throw new Error(`Module "${mod.name}" returned empty output`); } logLine("EXECUTE", step.action, stop(), step.description); return { query: step.description ?? input.query, data: output.data, context: input.context }; } catch (err) { logLine("EXECUTE", step.action, stop(), "failed"); throw err; } } /* ───────────── main run ───────────── */ async run() { this.runCount++; const stopRun = startTimer(); logLine("RUN", `start #${this.runCount}`); logInputOutput("GlobalContext (structured)", "input", this.context); this.spinner.start(); // start spinner at beginning /* BOOT */ { const t = startTimer(); await understandIntentStep.run({ context: this.context }); logLine("BOOT", "understandIntent", t()); } /* ROUTE */ { const t = startTimer(); await planResolverStep.run(this.context); logLine("ROUTE", "planResolver", t()); } const routing = this.context.analysis?.routingDecision; if (routing?.decision === "final-answer" && routing.answer) { this.spinner.stop(); logLine("RUN", "complete", stopRun()); userOutput("All input/output logs can be found at ~/.scai/input_output.log"); return { query: this.query, data: { finalAnswer: routing.answer, source: "planResolver" }, context: this.context }; } // ===================================================== // INFORMATION ACQUISITION PHASE // Purpose: gather raw information, no interpretation // ===================================================== { const t = startTimer(); await preFileSearchCheckStep(this.context); logLine("PRECHECK", "preFileSearch", t()); } { const t = startTimer(); await infoPlanGen.run(this.context); logLine("PLAN", "infoPlanGen", t()); } const infoPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] }; let stepIO = { query: this.query, context: this.context }; for (const step of infoPlan.steps.filter(s => s.groups?.includes("info"))) { stepIO = await this.executeStep(step, stepIO); } // ===================================================== // ANALYSIS PHASE // Purpose: understand what we have and what is being asked // ===================================================== { let t = startTimer(); await selectRelevantSourcesStep.run({ query: this.query, context: this.context }); logLine("ANALYSIS", "selectRelevantSources", t()); t = startTimer(); await structuralAnalysisStep.run({ query: this.query, context: this.context }); logLine("ANALYSIS", "structuralAnalysis", t()); t = startTimer(); await semanticAnalysisStep.run({ query: this.query, context: this.context }); logLine("ANALYSIS", "semanticAnalysis", t()); t = startTimer(); await planTargetFilesStep.run({ query: this.query, context: this.context }); logLine("ANALYSIS", "planTargetFiles", t()); t = startTimer(); await validationAnalysisStep.run({ query: this.query, context: this.context }); logLine("VALIDATE", "validationAnalysis", t()); } const review = await contextReviewStep(this.context); if (review.decision === "loopAgain" && this.runCount < this.maxRuns) { this.resetInitContextForLoop(); return this.run(); } // ===================================================== // TRANSFORM PHASE // Purpose: produce concrete changes or artifacts // ===================================================== { const t = startTimer(); await transformPlanGenStep.run(this.context); logLine("PLAN", "transformPlanGen", t()); } const transformPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] }; for (const step of transformPlan.steps.filter(s => s.groups?.includes("transform"))) { stepIO = await this.executeStep(step, stepIO); } // ===================================================== // FINALIZE PHASE // Purpose: commit results and respond to the user // ===================================================== { const t = startTimer(); await finalPlanGenStep.run(this.context); logLine("PLAN", "finalPlanGen", t()); } const finalPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] }; for (const step of finalPlan.steps.filter(s => s.groups?.includes("finalize"))) { stepIO = await this.executeStep(step, stepIO); } this.spinner.stop(); // stop spinner at the end userOutput("All input/output logs can be found at ~/.scai/input_output.log"); logLine("RUN", "complete", stopRun()); return stepIO; } /* ───────────── helpers ───────────── */ resetInitContextForLoop() { if (this.context.initContext) { this.context.initContext.relatedFiles = []; } } }