scai 0.1.168 → 0.1.170
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.
package/dist/agents/MainAgent.js
CHANGED
|
@@ -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]));
|
|
@@ -95,6 +96,10 @@ export class MainAgent {
|
|
|
95
96
|
let ready = false;
|
|
96
97
|
while (this.runCount < this.maxRuns) {
|
|
97
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());
|
|
98
103
|
const t1 = this.startTimer();
|
|
99
104
|
await evidenceVerifierStep.run({ query: this.query, context: this.context });
|
|
100
105
|
this.logLine("ANALYSIS", "collectAnalysisEvidence", t1());
|
|
@@ -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
|
|
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;
|
|
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,82 @@ export const evidenceVerifierStep = {
|
|
|
167
168
|
confidence: 1,
|
|
168
169
|
});
|
|
169
170
|
}
|
|
170
|
-
//
|
|
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
|
+
// -------- Compute file-level confidence --------
|
|
171
232
|
let fileScore = 0;
|
|
172
233
|
for (const ev of evidenceItems) {
|
|
173
234
|
if (ev.type === "sentence")
|
|
174
235
|
fileScore += 1.0;
|
|
175
236
|
else if (ev.type === "filename")
|
|
176
237
|
fileScore += 1.0;
|
|
177
|
-
else if (ev.type === "symbol")
|
|
238
|
+
else if (ev.type === "symbol" || ev.type === "structural")
|
|
178
239
|
fileScore += ev.confidence ?? 0.8;
|
|
179
240
|
}
|
|
180
|
-
// Normalize to 0–1 range (soft cap)
|
|
181
241
|
const fileConfidence = fileScore === 0
|
|
182
242
|
? 0
|
|
183
|
-
: Math.min(1, fileScore / 3);
|
|
243
|
+
: Math.min(1, fileScore / 3);
|
|
184
244
|
const isFocusFile = context.analysis.focus?.selectedFiles?.includes(path) ?? false;
|
|
185
245
|
const hasEvidence = evidenceItems.length > 0;
|
|
186
|
-
//
|
|
246
|
+
// -------- Merge into fileAnalysis --------
|
|
187
247
|
if (isFocusFile || hasEvidence) {
|
|
188
248
|
const confidenceLabel = fileConfidence.toFixed(2);
|
|
189
249
|
context.analysis.fileAnalysis[path] = {
|
|
@@ -229,7 +289,6 @@ export const evidenceVerifierStep = {
|
|
|
229
289
|
query,
|
|
230
290
|
data: { fileAnalysis: context.analysis.fileAnalysis },
|
|
231
291
|
};
|
|
232
|
-
// Build compact log summary
|
|
233
292
|
const logSummary = Object.entries(context.analysis.fileAnalysis).map(([path, analysis]) => {
|
|
234
293
|
const evidenceCount = analysis.evidence?.length ?? 0;
|
|
235
294
|
const confidenceMatch = analysis.relevanceExplanation?.match(/\[confidence:(\d+\.\d+)\]/);
|
|
@@ -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
|
+
};
|
|
@@ -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
|
|
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
|
|
229
|
+
const structural = {};
|
|
251
230
|
if (typeof fileId === "number") {
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
235
|
+
structural.kgTags = loadKgTags(db, p, MAX_KG_NEIGHBORS);
|
|
257
236
|
if (neighbors.length) {
|
|
258
|
-
|
|
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
|
-
|
|
245
|
+
structural.imports = imports.slice(0, 200);
|
|
267
246
|
if (exports.length)
|
|
268
|
-
|
|
247
|
+
structural.exports = exports.slice(0, 200);
|
|
269
248
|
}
|
|
270
|
-
|
|
271
|
-
|
|
249
|
+
structural.focusedTree = safeGenerateFocusedTree(p, 3);
|
|
250
|
+
result[p] = structural;
|
|
272
251
|
}
|
|
273
|
-
return
|
|
252
|
+
return result;
|
|
274
253
|
}
|