scai
Version:
> **A local-first AI CLI for understanding, querying, and iterating on large codebases.** > **100% local • No token costs • No cloud • No prompt injection • Private by design**
124 lines (116 loc) • 5.19 kB
JavaScript
// File: src/agents/researchPlanGenStep.ts
import { generate } from "../lib/generate.js";
import { cleanupModule } from "../pipeline/modules/cleanupModule.js";
import { logInputOutput } from "../utils/promptLogHelper.js";
const RESEARCH_ACTIONS = [
{
action: "research-impact-map",
description: "Map expected cross-file impact and affected areas before edits.",
},
{
action: "research-symbol-trace",
description: "Trace key symbols and call paths across candidate files.",
},
{
action: "research-risk-check",
description: "Identify risks, assumptions, and safety constraints.",
},
{
action: "research-architecture-synthesis",
description: "Synthesize architecture, hotspots, and coupling points from findings.",
},
];
/**
* RESEARCH PLAN GENERATOR
* Produces ordered research steps tailored to the current query and scope.
* Example: for repo-wide architecture questions, prioritize symbol-trace + synthesis.
*/
export const researchPlanGenStep = {
name: "researchPlanGen",
description: "Generates ordered research steps for repo-wide complex lanes.",
requires: ["analysis.intent", "analysis.scopeType", "analysis.routingDecision"],
produces: ["analysis.planSuggestion"],
async run(context) {
context.analysis || (context.analysis = {});
delete context.analysis.planSuggestion;
const intentText = context.analysis.intent?.normalizedQuery ??
context.initContext?.userQuery ??
"";
const intentCategory = context.analysis.intent?.intentCategory ?? "request";
const scopeType = context.analysis.scopeType ?? "repo-wide";
const selectedFiles = context.analysis.focus?.selectedFiles ?? [];
const candidateFiles = context.analysis.focus?.candidateFiles ?? [];
const prompt = `
You are generating a research-only execution plan for a coding agent.
User intent:
${intentText}
Intent category:
${intentCategory}
Scope:
${scopeType}
Selected files (current):
${JSON.stringify(selectedFiles.slice(0, 20), null, 2)}
Candidate files (current):
${JSON.stringify(candidateFiles.slice(0, 30), null, 2)}
Allowed research actions (use only these):
${JSON.stringify(RESEARCH_ACTIONS, null, 2)}
Rules:
- Return 2-4 ordered steps.
- Every step action must be one of the allowed research actions.
- Include architecture synthesis as final step when scope is multi-file or repo-wide.
- Prefer deterministic filePath values:
- research-impact-map => "__research__/impact-map"
- research-symbol-trace => "__research__/symbol-trace"
- research-risk-check => "__research__/risk-check"
- research-architecture-synthesis => "__research__/architecture-synthesis"
- Return strict JSON only:
{
"steps": [
{ "id": "research:1", "action": "research-impact-map", "targetFile": "__research__/impact-map", "description": "..." }
]
}
`.trim();
try {
const input = { query: intentText, content: prompt };
const generated = await generate(input);
const raw = typeof generated.data === "string"
? generated.data
: JSON.stringify(generated.data ?? "{}");
const cleaned = await cleanupModule.run({ query: intentText, content: raw });
const jsonString = typeof cleaned.content === "string"
? cleaned.content
: JSON.stringify(cleaned.content ?? "{}");
const parsed = JSON.parse(jsonString);
const candidateSteps = Array.isArray(parsed.steps) ? parsed.steps : [];
const allowedSet = new Set(RESEARCH_ACTIONS.map(a => a.action));
const normalized = candidateSteps
.filter(step => typeof step?.action === "string" && allowedSet.has(step.action))
.map((step, index) => {
const action = step.action;
const defaultFile = action === "research-impact-map"
? "__research__/impact-map"
: action === "research-symbol-trace"
? "__research__/symbol-trace"
: action === "research-risk-check"
? "__research__/risk-check"
: "__research__/architecture-synthesis";
return {
id: step.id ?? `research:${index + 1}`,
action,
targetFile: step.targetFile ?? defaultFile,
description: step.description ?? `Run ${action}`,
metadata: step.metadata ?? {},
groups: ["analysis", "planning"],
};
})
.slice(0, 4);
context.analysis.planSuggestion = { plan: { steps: normalized } };
logInputOutput("researchPlanGen", "output", { steps: normalized });
}
catch (err) {
console.warn("[researchPlanGenStep] Failed to generate research plan:", err);
context.analysis.planSuggestion = { plan: { steps: [] } };
logInputOutput("researchPlanGen", "output", { steps: [] });
}
},
};