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.
- package/dist/agents/MainAgent.js +69 -31
- package/dist/agents/evidenceVerifierStep.js +121 -10
- package/dist/agents/infoPlanGenStep.js +18 -31
- package/dist/agents/readinessGateStep.js +0 -1
- package/dist/agents/reasonNextTaskStep.js +14 -11
- package/dist/agents/scopeClassificationStep.js +49 -38
- package/dist/agents/structuralPreloadStep.js +61 -0
- package/dist/commands/AskCmd.js +4 -3
- package/dist/commands/TasksCmd.js +9 -3
- package/dist/db/fileIndex.js +33 -12
- package/dist/utils/buildContextualPrompt.js +12 -33
- package/package.json +1 -1
- package/dist/agents/routingDecisionStep.js +0 -69
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]));
|
|
@@ -55,12 +56,17 @@ export class MainAgent {
|
|
|
55
56
|
}
|
|
56
57
|
/* ───────────── main run ───────────── */
|
|
57
58
|
async run() {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 (
|
|
93
|
-
// ----------------
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 (
|
|
123
|
-
|
|
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}
|
|
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
|
|
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
|
|
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,122 @@ 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
|
+
// -------- 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);
|
|
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
|
-
//
|
|
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
|
-
|
|
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-
|
|
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'
|
|
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
|
|
24
|
-
|
|
25
|
-
//
|
|
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
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
//
|
|
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
|
|
99
|
-
|
|
100
|
-
context.analysis.planSuggestion
|
|
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);
|
|
@@ -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 (
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
//
|
|
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
|
|
30
|
+
You classify the scope of a user's request in a software repository.
|
|
11
31
|
|
|
12
|
-
Return STRICT JSON
|
|
32
|
+
Return STRICT JSON:
|
|
13
33
|
{
|
|
14
34
|
"scopeType": "none" | "single-file" | "multi-file" | "repo-wide"
|
|
15
35
|
}
|
|
16
36
|
|
|
17
|
-
|
|
18
|
-
- "none":
|
|
19
|
-
- "single-file":
|
|
20
|
-
- "multi-file":
|
|
21
|
-
- "repo-wide":
|
|
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:
|
|
24
|
-
|
|
25
|
-
|
|
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 &&
|
|
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
|
-
|
|
53
|
-
|
|
69
|
+
// ------------------------------------------------------------
|
|
70
|
+
// 3️⃣ Safe fallback if LLM fails
|
|
71
|
+
// ------------------------------------------------------------
|
|
72
|
+
if (!llmScope) {
|
|
73
|
+
llmScope = "repo-wide";
|
|
54
74
|
}
|
|
55
|
-
|
|
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:
|
|
68
|
-
finalScope,
|
|
69
|
-
reasoning:
|
|
78
|
+
fallbackScope: "repo-wide",
|
|
79
|
+
finalScope: llmScope,
|
|
80
|
+
reasoning: "llm-always",
|
|
70
81
|
});
|
|
71
|
-
return { scopeType:
|
|
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
|
+
};
|
package/dist/commands/AskCmd.js
CHANGED
|
@@ -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 (
|
|
23
|
-
|
|
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
|
-
|
|
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();
|
package/dist/db/fileIndex.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
154
|
+
- Output ONLY the OR-joined terms
|
|
155
|
+
- Max 12 total terms
|
|
144
156
|
- Use OR between terms
|
|
145
|
-
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
188
|
+
Generate 3–5 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
|
-
-
|
|
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,
|
|
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
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -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
|
-
};
|