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
JavaScript
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");
}
}
}