scai 0.1.167 → 0.1.169

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,6 +19,7 @@ import { selectRelevantSourcesStep } from "./selectRelevantSourcesStep.js";
19
19
  import { iterationFileSelector } from "./iterationFileSelector.js";
20
20
  import { finalAnswerModule } from "../pipeline/modules/finalAnswerModule.js";
21
21
  import { reasonNextStep } from "./reasonNextStep.js";
22
+ import { structuralPreloadStep } from "./structuralPreloadStep.js";
22
23
  import chalk from "chalk";
23
24
  /* ───────────────────────── registry ───────────────────────── */
24
25
  const MODULE_REGISTRY = Object.fromEntries(Object.entries(builtInModules).map(([name, mod]) => [name, mod]));
@@ -55,12 +56,17 @@ export class MainAgent {
55
56
  }
56
57
  /* ───────────── main run ───────────── */
57
58
  async run() {
58
- this.runCount = 0;
59
- await this.runBoot();
60
- await this.runScope();
61
- await this.runGrounding();
62
- await this.runWorkLoop();
63
- await this.runFinalize();
59
+ try {
60
+ this.runCount = 0;
61
+ await this.runBoot();
62
+ await this.runScope();
63
+ await this.runGrounding();
64
+ await this.runWorkLoop();
65
+ await this.runFinalize();
66
+ }
67
+ finally {
68
+ this.ui.stop(); // ← guaranteed cleanup
69
+ }
64
70
  }
65
71
  /* ───────────── boot ───────────── */
66
72
  async runBoot() {
@@ -86,31 +92,20 @@ export class MainAgent {
86
92
  await scopeClassificationStep.run(this.context);
87
93
  this.logLine("TASK", "Scope classification complete");
88
94
  }
89
- /* ───────────── grounding / info acquisition loop ───────────── */
90
95
  async runGrounding() {
91
96
  let ready = false;
92
- while (!ready && this.runCount < this.maxRuns) {
93
- // ---------------- INFORMATION ACQUISITION ----------------
94
- if (this.canExecutePhase("planning") &&
95
- this.canExecuteScope("planning")) {
96
- const t = this.startTimer();
97
- await infoPlanGenStep.run(this.context);
98
- const infoPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
99
- for (const step of infoPlan.steps) {
100
- const stepIO = { query: this.query };
101
- await this.executeStep(step, stepIO);
102
- }
103
- this.logLine("PLAN", "infoPlanGen", t(), undefined, { highlight: false });
104
- }
105
- // ---------------- DETERMINISTIC EVIDENCE VERIFICATION ----------------
97
+ while (this.runCount < this.maxRuns) {
98
+ // ---------------- EVIDENCE PIPELINE ----------------
99
+ // -------- STRUCTURAL PRELOAD --------
100
+ const t0 = this.startTimer();
101
+ await structuralPreloadStep.run({ query: this.query, context: this.context });
102
+ this.logLine("ANALYSIS", "structuralPreload", t0());
106
103
  const t1 = this.startTimer();
107
104
  await evidenceVerifierStep.run({ query: this.query, context: this.context });
108
105
  this.logLine("ANALYSIS", "collectAnalysisEvidence", t1());
109
- /* ───────────── precheck ───────────── */
110
106
  const t2 = this.startTimer();
111
107
  await fileCheckStep(this.context);
112
108
  this.logLine("ANALYSIS", "fileCheckStep", t2());
113
- // Select grounded candidate files
114
109
  const t3 = this.startTimer();
115
110
  await selectRelevantSourcesStep.run({ query: this.query, context: this.context });
116
111
  this.logLine("ANALYSIS", "selectRelevantSources", t3());
@@ -119,12 +114,29 @@ export class MainAgent {
119
114
  await readinessGateStep.run(this.context);
120
115
  this.logLine("HASINFO", "readinessGate", t4());
121
116
  ready = this.context.analysis?.readiness?.decision === "ready";
122
- if (!ready) {
123
- this.runCount++;
124
- if (this.context.analysis)
125
- this.context.analysis.planSuggestion = undefined;
126
- this.logLine("HASINFO", "Not ready — looping back to information acquisition", undefined, undefined, { highlight: false });
117
+ if (ready) {
118
+ break;
127
119
  }
120
+ // ---------------- INFORMATION ACQUISITION ----------------
121
+ if (this.canExecutePhase("planning") &&
122
+ this.canExecuteScope("planning")) {
123
+ const t = this.startTimer();
124
+ await infoPlanGenStep.run(this.context);
125
+ const infoPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
126
+ // If we are about to execute a new info acquisition wave,
127
+ // wipe previous search results.
128
+ if (infoPlan.steps.length > 0 && this.context.initContext && this.context.analysis?.focus) {
129
+ this.context.initContext.relatedFiles = [];
130
+ this.context.analysis.focus.candidateFiles = [];
131
+ }
132
+ for (const step of infoPlan.steps) {
133
+ const stepIO = { query: this.query };
134
+ await this.executeStep(step, stepIO);
135
+ }
136
+ this.logLine("PLAN", "infoPlanGen", t(), undefined, { highlight: false });
137
+ }
138
+ this.runCount++;
139
+ this.logLine("HASINFO", "Not ready — looping back to evidence collection", undefined, undefined, { highlight: false });
128
140
  }
129
141
  }
130
142
  /* ───────────── finalize ───────────── */
@@ -135,17 +147,43 @@ export class MainAgent {
135
147
  }
136
148
  /* ───────────── work loop ───────────── */
137
149
  async runWorkLoop() {
150
+ const readinessDecision = this.context.analysis?.readiness?.decision;
151
+ const readinessConfidence = this.context.analysis?.readiness?.confidence ?? 0;
152
+ if (readinessDecision !== "ready") {
153
+ // ❌ Graceful fallback instead of throwing
154
+ this.context.task.status = "deferred";
155
+ this.context.task.reason = `Readiness not achieved (decision=${readinessDecision}, confidence=${readinessConfidence})`;
156
+ persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
157
+ this.logLine("TASK", `Cannot start work loop — agent needs more evidence to safely proceed`, undefined, `Readiness: ${readinessDecision}, Confidence: ${readinessConfidence}`, { highlight: true });
158
+ return;
159
+ }
160
+ if (!this.context.task) {
161
+ throw new Error("runWorkLoop: missing task");
162
+ }
138
163
  const MAX_TASK_STEPS = 5;
139
164
  let stepCount = 0;
140
- while (stepCount < MAX_TASK_STEPS) {
165
+ while (stepCount < MAX_TASK_STEPS &&
166
+ this.context.task.status === "active") {
141
167
  await reasonNextTaskStep.run(this.context);
142
168
  const nextAction = this.context.analysis?.iterationReasoning?.nextAction;
169
+ // 🟡 Pause for user clarification
170
+ if (nextAction === "request-feedback") {
171
+ this.context.task.status = "paused";
172
+ persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
173
+ this.logLine("TASK", "Execution paused — awaiting user clarification", undefined, undefined, { highlight: false });
174
+ return;
175
+ }
176
+ // 🟢 Completed task
143
177
  if (nextAction === "complete") {
178
+ this.context.task.status = "completed";
179
+ persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
144
180
  this.logLine("TASK", "All selected files processed — task complete", undefined, undefined, { highlight: false });
145
181
  return;
146
182
  }
147
183
  const taskStep = await iterationFileSelector.run(this.context);
148
184
  if (!taskStep) {
185
+ this.context.task.status = "completed";
186
+ persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
149
187
  this.logLine("TASK", "No eligible taskStep found — task complete", undefined, undefined, { highlight: false });
150
188
  return;
151
189
  }
@@ -155,11 +193,11 @@ export class MainAgent {
155
193
  taskStep.stepIndex = stepCount;
156
194
  taskStep.status = "pending";
157
195
  persistTaskStepInsert(taskStep, getDbForRepo());
158
- this.logLine("NEW STEP", `Processing taskStep ${stepCount}/${MAX_TASK_STEPS}`, undefined, taskStep.filePath, { highlight: true });
196
+ this.logLine("NEW STEP", `Processing taskStep ${stepCount}`, undefined, taskStep.filePath, { highlight: true });
159
197
  taskStep.startTime = Date.now();
160
198
  persistTaskStepStart(taskStep, getDbForRepo());
161
199
  // ---------------------------
162
- // Step-level iterations: reason only about this file
200
+ // Step-level iterations
163
201
  // ---------------------------
164
202
  const stepAction = await this.runStepIterations(taskStep);
165
203
  if (stepAction === "complete") {
@@ -7,6 +7,7 @@ import { logInputOutput } from "../utils/promptLogHelper.js";
7
7
  * - Filters stopwords and short tokens.
8
8
  * - Deduplicates symbol evidence per file.
9
9
  * - Removes low-signal keyword clustering.
10
+ * - Strictly leverages structural data (functions, classes, imports/exports) for additional evidence.
10
11
  */
11
12
  export const evidenceVerifierStep = {
12
13
  name: "evidenceVerifier",
@@ -111,7 +112,7 @@ export const evidenceVerifierStep = {
111
112
  const evidenceItems = [];
112
113
  const matchedLines = [];
113
114
  const addedSymbols = new Set();
114
- // Sentence matches
115
+ // -------- Sentence matches --------
115
116
  lines.forEach((line, idx) => {
116
117
  sentenceTargets.forEach(target => {
117
118
  if (line.includes(target)) {
@@ -129,7 +130,7 @@ export const evidenceVerifierStep = {
129
130
  }
130
131
  });
131
132
  });
132
- // Symbol matches (deduplicated per symbol)
133
+ // -------- Symbol matches --------
133
134
  uniqueSymbolTargets.forEach(sym => {
134
135
  for (let idx = 0; idx < lines.length; idx++) {
135
136
  const line = lines[idx];
@@ -150,11 +151,11 @@ export const evidenceVerifierStep = {
150
151
  });
151
152
  matchedLines.push(line);
152
153
  }
153
- break; // stop after first meaningful match
154
+ break;
154
155
  }
155
156
  }
156
157
  });
157
- // Filename-level evidence
158
+ // -------- Filename-level evidence --------
158
159
  const fullFileName = path.split("/").pop() ?? "";
159
160
  const baseFileName = fullFileName.replace(/\.(ts|js|tsx|md)$/, "");
160
161
  if (filenameTargets.includes(fullFileName) ||
@@ -167,23 +168,122 @@ export const evidenceVerifierStep = {
167
168
  confidence: 1,
168
169
  });
169
170
  }
170
- // ----------------- Compute file-level confidence -----------------
171
+ // -------- Structural evidence (strict) --------
172
+ const struct = context.analysis.fileAnalysis[path]?.structural;
173
+ const structuralEvidence = [];
174
+ if (struct) {
175
+ const queryTokens = query
176
+ .toLowerCase()
177
+ .match(/\b\w{3,}\b/g) ?? [];
178
+ const querySet = new Set(queryTokens);
179
+ (struct.functions ?? []).forEach(fn => {
180
+ if (fn.name && querySet.has(fn.name.toLowerCase())) {
181
+ const ev = {
182
+ claim: `Function name matches query: "${fn.name}"`,
183
+ type: "structural",
184
+ excerpt: fn.name,
185
+ span: { startLine: fn.start ?? 0, endLine: fn.end ?? 0 },
186
+ confidence: 0.85,
187
+ };
188
+ evidenceItems.push(ev);
189
+ structuralEvidence.push(ev);
190
+ }
191
+ });
192
+ (struct.classes ?? []).forEach(cls => {
193
+ if (cls.name && querySet.has(cls.name.toLowerCase())) {
194
+ const ev = {
195
+ claim: `Class name matches query: "${cls.name}"`,
196
+ type: "structural",
197
+ excerpt: cls.name,
198
+ span: { startLine: cls.start ?? 0, endLine: cls.end ?? 0 },
199
+ confidence: 0.85,
200
+ };
201
+ evidenceItems.push(ev);
202
+ structuralEvidence.push(ev);
203
+ }
204
+ });
205
+ [...(struct.imports ?? []), ...(struct.exports ?? [])].forEach(sym => {
206
+ if (sym && querySet.has(sym.toLowerCase())) {
207
+ const ev = {
208
+ claim: `Import/Export matches query: "${sym}"`,
209
+ type: "structural",
210
+ excerpt: sym,
211
+ span: { startLine: 0, endLine: 0 },
212
+ confidence: 0.85,
213
+ };
214
+ evidenceItems.push(ev);
215
+ structuralEvidence.push(ev);
216
+ }
217
+ });
218
+ // -------- Log structural evidence per file --------
219
+ if (structuralEvidence.length > 0) {
220
+ logInputOutput("evidenceVerifier", "output", {
221
+ file: path,
222
+ count: structuralEvidence.length,
223
+ examples: structuralEvidence.slice(0, 5).map(ev => ({
224
+ claim: ev.claim,
225
+ excerpt: ev.excerpt,
226
+ confidence: ev.confidence,
227
+ })),
228
+ });
229
+ }
230
+ }
231
+ // -------- Structural evidence (strict) --------
232
+ if (struct) {
233
+ const queryTokens = query
234
+ .toLowerCase()
235
+ .match(/\b\w{3,}\b/g) ?? [];
236
+ const querySet = new Set(queryTokens);
237
+ (struct.functions ?? []).forEach(fn => {
238
+ if (fn.name && querySet.has(fn.name.toLowerCase())) {
239
+ evidenceItems.push({
240
+ claim: `Function name matches query: "${fn.name}"`,
241
+ type: "structural",
242
+ excerpt: fn.name,
243
+ span: { startLine: fn.start ?? 0, endLine: fn.end ?? 0 },
244
+ confidence: 0.85,
245
+ });
246
+ }
247
+ });
248
+ (struct.classes ?? []).forEach(cls => {
249
+ if (cls.name && querySet.has(cls.name.toLowerCase())) {
250
+ evidenceItems.push({
251
+ claim: `Class name matches query: "${cls.name}"`,
252
+ type: "structural",
253
+ excerpt: cls.name,
254
+ span: { startLine: cls.start ?? 0, endLine: cls.end ?? 0 },
255
+ confidence: 0.85,
256
+ });
257
+ }
258
+ });
259
+ [...(struct.imports ?? []), ...(struct.exports ?? [])].forEach(sym => {
260
+ if (sym && querySet.has(sym.toLowerCase())) {
261
+ evidenceItems.push({
262
+ claim: `Import/Export matches query: "${sym}"`,
263
+ type: "structural",
264
+ excerpt: sym,
265
+ span: { startLine: 0, endLine: 0 },
266
+ confidence: 0.85,
267
+ });
268
+ }
269
+ });
270
+ }
271
+ // -------- Compute file-level confidence --------
171
272
  let fileScore = 0;
172
273
  for (const ev of evidenceItems) {
173
274
  if (ev.type === "sentence")
174
275
  fileScore += 1.0;
175
276
  else if (ev.type === "filename")
176
277
  fileScore += 1.0;
177
- else if (ev.type === "symbol")
278
+ else if (ev.type === "symbol" || ev.type === "structural")
178
279
  fileScore += ev.confidence ?? 0.8;
179
280
  }
180
- // Normalize to 0–1 range (soft cap)
181
281
  const fileConfidence = fileScore === 0
182
282
  ? 0
183
- : Math.min(1, fileScore / 3); // 3 strong signals = max confidence
283
+ : Math.min(1, fileScore / 3);
184
284
  const isFocusFile = context.analysis.focus?.selectedFiles?.includes(path) ?? false;
185
285
  const hasEvidence = evidenceItems.length > 0;
186
- // ----------------- Merge into fileAnalysis -----------------
286
+ // -------- Merge into fileAnalysis --------
187
287
  if (isFocusFile || hasEvidence) {
188
288
  const confidenceLabel = fileConfidence.toFixed(2);
189
289
  context.analysis.fileAnalysis[path] = {
@@ -229,7 +329,18 @@ export const evidenceVerifierStep = {
229
329
  query,
230
330
  data: { fileAnalysis: context.analysis.fileAnalysis },
231
331
  };
232
- logInputOutput("evidenceVerifier", "output", output.data);
332
+ const logSummary = Object.entries(context.analysis.fileAnalysis).map(([path, analysis]) => {
333
+ const evidenceCount = analysis.evidence?.length ?? 0;
334
+ const confidenceMatch = analysis.relevanceExplanation?.match(/\[confidence:(\d+\.\d+)\]/);
335
+ const confidence = confidenceMatch?.[1] ?? "0.00";
336
+ return {
337
+ file: path,
338
+ confidence,
339
+ evidenceCount,
340
+ isRelevant: analysis.action?.isRelevant ?? false,
341
+ };
342
+ });
343
+ logInputOutput("evidenceVerifier", "output", logSummary);
233
344
  return output;
234
345
  },
235
346
  };
@@ -5,40 +5,32 @@ import { logInputOutput } from '../utils/promptLogHelper.js';
5
5
  import { cleanupModule } from '../pipeline/modules/cleanupModule.js';
6
6
  /**
7
7
  * INFO PLAN GENERATOR
8
- * Generates a single information-gathering step.
8
+ * Generates a single information-acquisition step.
9
+ *
10
+ * NOTE:
11
+ * - This step only creates a plan; execution happens in the agent loop.
12
+ * - The agent controls when this step runs.
9
13
  */
10
14
  export const infoPlanGenStep = {
11
15
  name: 'infoPlanGen',
12
16
  description: 'Generates one information-acquisition step.',
13
- requires: ['userQuery', 'analysis.intent', 'analysis.focus'],
17
+ requires: ['userQuery', 'analysis.intent'],
14
18
  produces: ['analysis.planSuggestion'],
15
19
  async run(context) {
16
20
  context.analysis || (context.analysis = {});
17
21
  // Clear any previous plan
18
22
  delete context.analysis.planSuggestion;
19
- const missingFiles = Array.isArray(context.analysis.focus?.candidateFiles)
20
- ? context.analysis.focus.candidateFiles
21
- : [];
22
23
  const analysis = context.analysis;
23
- const needsDiscovery = analysis?.routingDecision?.decision === "needs-info" ||
24
- analysis?.scopeType === "repo-wide";
25
- // Nothing to do? Save empty steps array
26
- if (!needsDiscovery && missingFiles.length === 0) {
27
- context.analysis.planSuggestion = { plan: { steps: [] } };
28
- logInputOutput("infoPlanGen", "output", []);
29
- return;
30
- }
31
- // Restrict actions to INFO only
24
+ const intentText = analysis.intent?.normalizedQuery ?? '';
25
+ const intentCategory = analysis.intent?.intentCategory ?? '';
26
+ // Only info-type actions
32
27
  const effectiveActions = PLAN_ACTIONS.filter(a => a.groups?.includes('info'));
33
- // No actions available? Save empty steps array
34
28
  if (!effectiveActions.length) {
35
29
  context.analysis.planSuggestion = { plan: { steps: [] } };
36
30
  logInputOutput('infoPlanGen', 'output', []);
37
31
  return;
38
32
  }
39
33
  const actionsJson = JSON.stringify(effectiveActions, null, 2);
40
- const intentText = analysis.intent?.normalizedQuery ?? '';
41
- const intentCategory = analysis.intent?.intentCategory ?? '';
42
34
  const prompt = `
43
35
  You are an autonomous coding agent.
44
36
 
@@ -53,16 +45,10 @@ ${intentCategory}
53
45
  Allowed actions (info only):
54
46
  ${actionsJson}
55
47
 
56
- Existing relevant files:
57
- ${JSON.stringify(analysis.focus?.selectedFiles ?? [], null, 2)}
58
-
59
- Missing files / candidates:
60
- ${JSON.stringify(missingFiles, null, 2)}
61
-
62
48
  Rules:
63
49
  - Only produce a single info step.
64
50
  - Step must include: "action", "description", "subQuery" (array), "metadata".
65
- - Do NOT invent new files or actions.
51
+ - Do NOT invent new actions or files.
66
52
  - Return strictly valid JSON representing one step.
67
53
 
68
54
  If no further information is required, return:
@@ -78,12 +64,14 @@ If no further information is required, return:
78
64
  const jsonString = typeof cleaned.content === 'string'
79
65
  ? cleaned.content
80
66
  : JSON.stringify(cleaned.content ?? '{}');
81
- let step = JSON.parse(jsonString);
82
- // Validate structure
67
+ // Unwrap the step from LLM output
68
+ const parsed = JSON.parse(jsonString);
69
+ let step = parsed?.step ?? null;
70
+ // Validate minimal structure
83
71
  if (!step || typeof step.action !== 'string') {
84
72
  step = null;
85
73
  }
86
- // Map groups & metadata
74
+ // Attach groups & routing confidence if available
87
75
  if (step) {
88
76
  const actionDef = PLAN_ACTIONS.find(a => a.action === step.action);
89
77
  step.groups = actionDef?.groups ?? ['info'];
@@ -95,10 +83,9 @@ If no further information is required, return:
95
83
  : {})
96
84
  };
97
85
  }
98
- // Save single step to context as an array (empty if null)
99
- const steps = step ? [step] : [];
100
- context.analysis.planSuggestion = { plan: { steps } };
101
- logInputOutput('infoPlanGen', 'output', steps);
86
+ // Save as array in planSuggestion
87
+ context.analysis.planSuggestion = { plan: { steps: step ? [step] : [] } };
88
+ logInputOutput('infoPlanGen', 'output', context.analysis.planSuggestion.plan?.steps ?? []);
102
89
  }
103
90
  catch (err) {
104
91
  console.warn('⚠️ Failed to generate info step:', err);
@@ -131,6 +131,5 @@ function logOutput(context) {
131
131
  logInputOutput('readinessGateStep', 'output', {
132
132
  readiness: context.analysis?.readiness,
133
133
  focus: context.analysis?.focus,
134
- fileAnalysis: context.analysis?.fileAnalysis,
135
134
  });
136
135
  }
@@ -1,6 +1,3 @@
1
- // File: src/agents/reasonNextTaskStep.ts
2
- import { generate } from "../lib/generate.js";
3
- import { cleanupModule } from "../pipeline/modules/cleanupModule.js";
4
1
  import { logInputOutput } from "../utils/promptLogHelper.js";
5
2
  /**
6
3
  * REASON NEXT TASK STEP
@@ -132,10 +129,11 @@ export const reasonNextTaskStep = {
132
129
  rationale = "All selected files have been analyzed, transformed, and validated successfully.";
133
130
  confidence = 0.98;
134
131
  }
135
- // ---------------------------
132
+ /* // ---------------------------
136
133
  // 6.5️⃣ Optional: Reason over known risks
137
134
  // ---------------------------
138
135
  const knownRisks = context.analysis.understanding?.risks ?? [];
136
+
139
137
  if (knownRisks.length > 0) {
140
138
  // Optionally call the LLM with constrained instructions
141
139
  const riskPrompt = `
@@ -146,34 +144,39 @@ Task:
146
144
  - Decide whether it is reasonable to ask the user for clarification before proceeding.
147
145
  - Return STRICT JSON: { askUser: true|false, rationale: string }
148
146
  `;
147
+
149
148
  try {
150
149
  const aiResponse = await generate({
151
150
  query: context.initContext?.userQuery ?? "",
152
151
  content: riskPrompt
153
152
  });
153
+
154
154
  const cleaned = await cleanupModule.run({
155
155
  query: context.initContext?.userQuery ?? "",
156
156
  content: aiResponse.data ?? ""
157
157
  });
158
+
158
159
  const parsed = cleaned.data;
160
+
159
161
  // type guard
160
- if (parsed &&
162
+ if (
163
+ parsed &&
161
164
  typeof parsed === "object" &&
162
165
  "askUser" in parsed &&
163
166
  "rationale" in parsed &&
164
- typeof parsed.rationale === "string") {
165
- if (parsed.askUser) {
167
+ typeof (parsed as { rationale?: unknown }).rationale === "string"
168
+ ) {
169
+ if ((parsed as { askUser: boolean }).askUser) {
166
170
  nextAction = "request-feedback";
167
- rationale += `\nUser clarification recommended due to known risks: ${parsed.rationale}`;
171
+ rationale += `\nUser clarification recommended due to known risks: ${(parsed as { rationale: string }).rationale}`;
168
172
  confidence = Math.min(confidence, 0.8); // slightly lower because human needed
169
173
  }
170
174
  }
171
- }
172
- catch (err) {
175
+ } catch (err) {
173
176
  console.warn("[reasonNextTaskStep] Risk reasoning failed", err);
174
177
  // fallback: ignore, keep deterministic nextAction
175
178
  }
176
- }
179
+ } */
177
180
  // ---------------------------
178
181
  // 7️⃣ Ensure a TaskStep exists for nextFile
179
182
  // ---------------------------
@@ -4,70 +4,81 @@ import { logInputOutput } from "../utils/promptLogHelper.js";
4
4
  export const scopeClassificationStep = {
5
5
  run: async (context) => {
6
6
  const query = context.initContext?.userQuery?.trim() ?? "";
7
- const repoFiles = context.initContext?.relatedFiles?.join(", ") ?? "";
8
- // ------------------------ LLM PROMPT ------------------------
7
+ context.analysis ?? (context.analysis = {});
8
+ // ------------------------------------------------------------
9
+ // 1️⃣ Prepare deterministic hints for the LLM
10
+ // ------------------------------------------------------------
11
+ const lower = query.toLowerCase();
12
+ const hints = [];
13
+ // Systemic/repo-wide hints
14
+ if (/\b(codebase|entire repo|whole repo|entire project|whole project|application|system)\b/i.test(lower)) {
15
+ hints.push("The query mentions the entire codebase or system; likely repo-wide scope.");
16
+ }
17
+ // Debugging or error hints
18
+ if (/\b(debug|issue|bug|memory leak|performance|error|crash|failure)\b/i.test(lower)) {
19
+ hints.push("The query mentions debugging, errors, or memory/performance issues; consider broad impact.");
20
+ }
21
+ // Explicit artifact references
22
+ if (/\b[\w-]+\.(ts|js|tsx|jsx|py|java|go|rs|cpp|c|cs)\b/i.test(lower) ||
23
+ /\b(class|function|method|module)\s+\w+/i.test(lower)) {
24
+ hints.push("The query references a specific file, class, function, or module; possibly single-file scope.");
25
+ }
26
+ // ------------------------------------------------------------
27
+ // 2️⃣ LLM classification
28
+ // ------------------------------------------------------------
9
29
  const prompt = `
10
- You are an assistant that classifies the "scope" of a user's request in a code repository.
30
+ You classify the scope of a user's request in a software repository.
11
31
 
12
- Return STRICT JSON using this schema:
32
+ Return STRICT JSON:
13
33
  {
14
34
  "scopeType": "none" | "single-file" | "multi-file" | "repo-wide"
15
35
  }
16
36
 
17
- Rules:
18
- - "none": request does not require touching any repository files.
19
- - "single-file": request likely affects only one file.
20
- - "multi-file": request affects several files or modules.
21
- - "repo-wide": request affects many files or the whole repo.
37
+ Definitions:
38
+ - "none": purely conceptual, no repository interaction required.
39
+ - "single-file": clearly limited to one specific file or artifact.
40
+ - "multi-file": involves several related files/modules.
41
+ - "repo-wide": broad/systemic across the repository.
22
42
 
23
- User query: "${query}"
24
- Candidate files: [${repoFiles}]
25
- `.trim();
43
+ User query:
44
+ "${query}"
45
+
46
+ Hints for classification:
47
+ ${hints.length ? "- " + hints.join("\n- ") : "None"}
48
+ `.trim();
26
49
  let llmScope = null;
27
50
  try {
28
- // Step 1: ask LLM
29
51
  const genInput = { query, content: prompt };
30
52
  const genOutput = await generate(genInput);
31
53
  const raw = typeof genOutput.data === "string"
32
54
  ? genOutput.data
33
55
  : JSON.stringify(genOutput.data ?? "{}");
34
- // Step 2: cleanup LLM output (remove extra text, comments, etc.)
35
56
  const cleaned = await cleanupModule.run({ query, content: raw });
36
57
  const jsonString = typeof cleaned.content === "string"
37
58
  ? cleaned.content
38
59
  : JSON.stringify(cleaned.content ?? "{}");
39
60
  const parsed = JSON.parse(jsonString);
40
- if (parsed && ["none", "single-file", "multi-file", "repo-wide"].includes(parsed.scopeType)) {
61
+ if (parsed &&
62
+ ["none", "single-file", "multi-file", "repo-wide"].includes(parsed.scopeType)) {
41
63
  llmScope = parsed.scopeType;
42
64
  }
43
65
  }
44
66
  catch (err) {
45
- console.warn("LLM scope classification failed:", err);
46
- }
47
- // ------------------------ FALLBACK HEURISTICS ------------------------
48
- let fallbackScope = "repo-wide";
49
- if (!query || /\b(explain|what|who|define|describe|overview|summary)\b/i.test(query)) {
50
- fallbackScope = "none";
67
+ console.warn("⚠️ LLM scope classification failed, falling back:", err);
51
68
  }
52
- else if (/\b(file|module|class|function|cleanupModule\.ts|MainAgent\.ts|example\.ts)\b/i.test(query)) {
53
- fallbackScope = "single-file";
69
+ // ------------------------------------------------------------
70
+ // 3️⃣ Safe fallback if LLM fails
71
+ // ------------------------------------------------------------
72
+ if (!llmScope) {
73
+ llmScope = "repo-wide";
54
74
  }
55
- else if (/\b(across|multiple files|several files|refactor|update many|shared modules)\b/i.test(query)) {
56
- fallbackScope = "multi-file";
57
- }
58
- // ------------------------ FINAL DECISION ------------------------
59
- const finalScope = llmScope ?? fallbackScope;
60
- // Save in context
61
- context.analysis ?? (context.analysis = {});
62
- context.analysis.scopeType = finalScope;
63
- const scopeReasoning = llmScope ? "llm" : "heuristic-fallback";
64
- // Log both for monitoring
75
+ context.analysis.scopeType = llmScope;
65
76
  logInputOutput("scopeClassificationStep", "output", {
66
77
  llmScope,
67
- fallbackScope: fallbackScope,
68
- finalScope,
69
- reasoning: scopeReasoning,
78
+ fallbackScope: "repo-wide",
79
+ finalScope: llmScope,
80
+ reasoning: "llm-always",
70
81
  });
71
- return { scopeType: finalScope };
72
- }
82
+ return { scopeType: llmScope };
83
+ },
73
84
  };
@@ -0,0 +1,61 @@
1
+ // File: src/modules/structuralPreloadStep.ts
2
+ import { buildInDepthContext } from "../utils/buildContextualPrompt.js";
3
+ import { logInputOutput } from "../utils/promptLogHelper.js";
4
+ /**
5
+ * Structural preload:
6
+ * - Calls buildInDepthContext for candidate files.
7
+ * - Extracts structural facts only.
8
+ * - Populates analysis.fileAnalysis[path].structural.
9
+ * - Does NOT reason or assign relevance.
10
+ */
11
+ export const structuralPreloadStep = {
12
+ name: "structuralPreload",
13
+ description: "Preloads structural KG and code metadata into fileAnalysis without performing reasoning.",
14
+ groups: ["analysis"],
15
+ run: async (input) => {
16
+ const query = input.query ?? "";
17
+ const context = input.context;
18
+ if (!context?.analysis) {
19
+ throw new Error("[structuralPreload] context.analysis is required.");
20
+ }
21
+ // ---- Ensure fileAnalysis exists (type-safe) ----
22
+ if (!context.analysis.fileAnalysis) {
23
+ context.analysis.fileAnalysis = {};
24
+ }
25
+ const fileAnalysis = context.analysis.fileAnalysis;
26
+ // ---- Candidate files ----
27
+ const candidatePaths = [
28
+ ...(context.initContext?.relatedFiles ?? []),
29
+ ];
30
+ const uniquePaths = Array.from(new Set(candidatePaths));
31
+ if (!uniquePaths.length) {
32
+ console.warn("[structuralPreload] No candidate files to preload.");
33
+ return { query, data: {} };
34
+ }
35
+ // ---- Only preload missing structural data ----
36
+ const pathsNeedingStructure = uniquePaths.filter((p) => !fileAnalysis[p]?.structural);
37
+ if (!pathsNeedingStructure.length)
38
+ return { query, data: {} };
39
+ // ---- Get structural data (path → structural object) ----
40
+ const structuralMap = await buildInDepthContext({
41
+ filenames: pathsNeedingStructure,
42
+ relatedFiles: context.initContext?.relatedFiles ?? [],
43
+ query,
44
+ });
45
+ // ---- Merge into fileAnalysis ----
46
+ for (const [path, structural] of Object.entries(structuralMap)) {
47
+ fileAnalysis[path] ?? (fileAnalysis[path] = { semanticAnalyzed: false });
48
+ fileAnalysis[path].structural = structural;
49
+ }
50
+ // ---- Minimal structured log ----
51
+ const logSummary = Object.entries(fileAnalysis).map(([path, analysis]) => ({
52
+ file: path,
53
+ hasStructural: !!analysis.structural,
54
+ functions: analysis.structural?.functions?.length ?? 0,
55
+ classes: analysis.structural?.classes?.length ?? 0,
56
+ imports: analysis.structural?.imports?.length ?? 0,
57
+ }));
58
+ logInputOutput("structuralPreload", "output", logSummary);
59
+ return { query, data: {} };
60
+ },
61
+ };
@@ -65,16 +65,17 @@ export async function runAskCommand(query) {
65
65
  const ui = {
66
66
  update: text => spinner.update(text),
67
67
  pause: fn => {
68
- spinner.stop();
68
+ spinner.stop(); // pause spinner
69
69
  try {
70
70
  fn();
71
71
  }
72
72
  finally {
73
- spinner.start();
73
+ spinner.start(); // resume spinner
74
74
  }
75
75
  },
76
76
  succeed: msg => spinner.succeed(msg),
77
- fail: msg => spinner.fail(msg)
77
+ fail: msg => spinner.fail(msg),
78
+ stop: () => spinner.stop() // ✅ proper stop implementation
78
79
  };
79
80
  const agent = new MainAgent(context, ui);
80
81
  await agent.run();
@@ -19,12 +19,18 @@ export async function runTasksCommand() {
19
19
  console.log(`${chalk.cyan(`[${t.id}]`)} ${chalk.yellow(t.status.padEnd(9))} ${oneLineTruncate(t.initial_query)}`);
20
20
  }
21
21
  await new Promise((resolve) => {
22
- rl.question("\nSelect task id (enter to quit): ", answer => {
23
- if (!answer.trim() || answer.trim().toLowerCase() === "q") {
22
+ rl.question("\nSelect task id (q to quit): ", answer => {
23
+ const trimmed = answer.trim();
24
+ if (trimmed.toLowerCase() === "q") {
24
25
  resolve();
25
26
  return;
26
27
  }
27
- const id = Number(answer);
28
+ if (!trimmed) {
29
+ console.log(chalk.red("Please enter a task id or 'q' to quit."));
30
+ resolve();
31
+ return;
32
+ }
33
+ const id = Number(trimmed);
28
34
  if (!Number.isInteger(id)) {
29
35
  console.log(chalk.red("Invalid task id."));
30
36
  resolve();
@@ -130,21 +130,33 @@ export async function semanticSearchFiles(originalQuery, _query, topK = 5) {
130
130
  return [];
131
131
  }
132
132
  /* -------------------------------------------------- */
133
- /* LLM → FTS QUERY GENERATION (TAG-BASED) */
133
+ /* LLM → FTS QUERY GENERATION (TAG-BASED) */
134
134
  /* -------------------------------------------------- */
135
135
  async function generatePrimaryFtsQuery(userQuery) {
136
136
  const prompt = `
137
- Generate a SQLite FTS query for searching a source code repository.
137
+ You are generating a SQLite FTS query for searching a source code repository.
138
+
139
+ The user query may refer to:
140
+ - High-level intent
141
+ - Domain terminology
142
+ - Specific filenames, file types, or configuration files
138
143
 
139
144
  Input:
140
145
  "${userQuery}"
141
146
 
147
+ Task:
148
+ 1. Extract high-level intent terms
149
+ 2. Expand to related domain-specific terminology
150
+ 3. Expand to likely filenames, config files, or structural artifacts if relevant
151
+ 4. Combine ALL useful terms into ONE OR-joined FTS query
152
+
142
153
  Rules:
143
- - Output ONLY the query terms
154
+ - Output ONLY the OR-joined terms
155
+ - Max 12 total terms
144
156
  - Use OR between terms
145
- - Max 10 terms
157
+ - Include filenames when relevant
146
158
  - No explanations
147
- - No sentences
159
+ - No natural language sentences
148
160
 
149
161
  Wrap the result in <FILE_CONTENT> tags.
150
162
 
@@ -164,7 +176,7 @@ term1 OR term2 OR term3
164
176
  }
165
177
  async function generateFallbackFtsQueries(userQuery, failedQuery) {
166
178
  const prompt = `
167
- You are generating fallback SQLite FTS queries for a source code search.
179
+ You are generating fallback SQLite FTS queries for a source code repository search.
168
180
 
169
181
  Original user query:
170
182
  "${userQuery}"
@@ -173,18 +185,27 @@ Primary FTS query returned ZERO results:
173
185
  "${failedQuery}"
174
186
 
175
187
  Task:
176
- - Generate 23 independent FTS queries (MAX 3)
177
- - Each query must be a single OR-joined expression
178
- - Max 10 terms per query
179
- - Focus on filenames, symbols, module names
188
+ Generate 35 independent FTS queries (MAX 5).
189
+
190
+ For each query:
191
+ 1. Think at a different abstraction level (intent-level, domain-level, structural-level).
192
+ 2. Include filenames, file types, modules, config files, or symbols when relevant.
193
+ 3. Use a single OR-joined expression.
194
+ 4. Max 10 terms per query.
195
+
196
+ Rules:
180
197
  - Avoid natural language sentences
181
- - Avoid explanations or commentary
198
+ - No explanations
199
+ - No commentary
200
+ - Each line must be one complete OR expression
182
201
 
183
202
  Output format (STRICT):
184
203
  <FILE_CONTENT>
185
204
  query1
186
205
  query2
187
206
  query3
207
+ query4
208
+ query5
188
209
  </FILE_CONTENT>
189
210
  `.trim();
190
211
  try {
@@ -195,7 +216,7 @@ query3
195
216
  .split(/\r?\n/)
196
217
  .map(q => sanitizeQueryForFts(q.trim()))
197
218
  .filter(Boolean)
198
- .slice(0, 3);
219
+ .slice(0, 5);
199
220
  if (!subQueries.length) {
200
221
  throw new Error("No fallback subqueries generated");
201
222
  }
@@ -218,44 +218,23 @@ function extractFileReferences(query) {
218
218
  return [...new Set(matches)];
219
219
  }
220
220
  /* ======================================================
221
- IN-DEPTH CONTEXT
221
+ IN-DEPTH CONTEXT (Structural-Only)
222
222
  ====================================================== */
223
223
  export async function buildInDepthContext({ filenames, kgDepth = DEFAULT_KG_DEPTH, relatedFiles, query, }) {
224
224
  const db = getDbForRepo();
225
225
  const safeFilenames = Array.isArray(filenames) ? filenames : [];
226
- const safeRelated = Array.isArray(relatedFiles) ? relatedFiles : [];
227
- const initCtx = {
228
- userQuery: query?.trim() || "",
229
- repoTree: safeGenerateRepoTree(3),
230
- relatedFiles: safeRelated,
231
- folderCapsules: loadRelevantFolderCapsules(normalizeToFolders(safeRelated)),
232
- };
233
- const workingFiles = [];
234
- const out = {
235
- initContext: initCtx,
236
- workingFiles,
237
- task: {
238
- id: 0,
239
- projectId: 0,
240
- status: "active",
241
- initialQuery: query?.trim() ?? "",
242
- createdAt: new Date().toISOString(),
243
- updatedAt: new Date().toISOString(),
244
- taskSteps: [],
245
- },
246
- };
247
- /* -------- Working files (deep phase only) -------- */
226
+ const result = {};
248
227
  for (const p of safeFilenames) {
249
228
  const fileId = fileRowIdForPath(db, p);
250
- const fileObj = { path: p };
229
+ const structural = {};
251
230
  if (typeof fileId === "number") {
252
- fileObj.functions = loadFunctions(db, fileId, MAX_FUNCTIONS);
253
- fileObj.classes = loadClasses(db, fileId, 200);
231
+ structural.functions = loadFunctions(db, fileId, MAX_FUNCTIONS);
232
+ structural.classes = loadClasses(db, fileId, 200);
254
233
  }
255
234
  const neighbors = loadKgNeighbors(db, p, MAX_KG_NEIGHBORS);
256
- fileObj.kgTags = loadKgTags(db, p, MAX_KG_NEIGHBORS);
235
+ structural.kgTags = loadKgTags(db, p, MAX_KG_NEIGHBORS);
257
236
  if (neighbors.length) {
258
- fileObj.kgNeighborhood = neighbors.map((e) => `${e.relation}:${e.target}`);
237
+ structural.kgNeighborhood = neighbors.map((e) => `${e.relation}:${e.target}`);
259
238
  const imports = neighbors
260
239
  .filter((e) => e.relation === "imports")
261
240
  .map((e) => e.target);
@@ -263,12 +242,12 @@ export async function buildInDepthContext({ filenames, kgDepth = DEFAULT_KG_DEPT
263
242
  .filter((e) => e.relation === "exports")
264
243
  .map((e) => e.target);
265
244
  if (imports.length)
266
- fileObj.imports = imports.slice(0, 200);
245
+ structural.imports = imports.slice(0, 200);
267
246
  if (exports.length)
268
- fileObj.exports = exports.slice(0, 200);
247
+ structural.exports = exports.slice(0, 200);
269
248
  }
270
- fileObj.focusedTree = safeGenerateFocusedTree(p, 3);
271
- workingFiles.push(fileObj);
249
+ structural.focusedTree = safeGenerateFocusedTree(p, 3);
250
+ result[p] = structural;
272
251
  }
273
- return out;
252
+ return result;
274
253
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.167",
3
+ "version": "0.1.169",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"
@@ -1,69 +0,0 @@
1
- import { logInputOutput } from "../utils/promptLogHelper.js";
2
- export const routingDecisionStep = {
3
- run: async (context) => {
4
- context.analysis ?? (context.analysis = {});
5
- const scope = context.analysis.scopeType ?? "repo-wide";
6
- // Default routing decision
7
- let routing = {
8
- decision: "has-info",
9
- allowSearch: false,
10
- allowInfoSteps: false,
11
- allowTransform: false,
12
- allowExplain: false,
13
- scopeLocked: false,
14
- rationale: ""
15
- };
16
- switch (scope) {
17
- case "none":
18
- routing = {
19
- decision: "has-info",
20
- allowSearch: false,
21
- allowInfoSteps: false,
22
- allowTransform: false,
23
- allowExplain: true,
24
- scopeLocked: true,
25
- rationale: "Query is not repo-related, early explain exit allowed"
26
- };
27
- break;
28
- case "single-file":
29
- routing = {
30
- decision: "has-info",
31
- allowSearch: true,
32
- allowInfoSteps: true,
33
- allowTransform: true,
34
- allowExplain: false,
35
- scopeLocked: true,
36
- rationale: "Query targets a single file; safe to execute info and transform steps"
37
- };
38
- break;
39
- case "multi-file":
40
- routing = {
41
- decision: "needs-info",
42
- allowSearch: true,
43
- allowInfoSteps: true,
44
- allowTransform: true,
45
- allowExplain: false,
46
- scopeLocked: false,
47
- rationale: "Query spans multiple files; controlled search and info acquisition needed"
48
- };
49
- break;
50
- case "repo-wide":
51
- routing = {
52
- decision: "needs-info",
53
- allowSearch: true,
54
- allowInfoSteps: true,
55
- allowTransform: true,
56
- allowExplain: false,
57
- scopeLocked: false,
58
- rationale: "Query is repo-wide; full search and sampling required"
59
- };
60
- break;
61
- }
62
- context.analysis.routingDecision = routing;
63
- logInputOutput("routingDecisionStep", "output", {
64
- routingDecision: routing,
65
- scope,
66
- });
67
- return routing;
68
- }
69
- };