scai 0.1.167 → 0.1.168
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 +64 -31
- package/dist/agents/evidenceVerifierStep.js +13 -1
- 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/commands/AskCmd.js +4 -3
- package/dist/commands/TasksCmd.js +9 -3
- package/dist/db/fileIndex.js +33 -12
- package/package.json +1 -1
- package/dist/agents/routingDecisionStep.js +0 -69
package/dist/agents/MainAgent.js
CHANGED
|
@@ -55,12 +55,17 @@ export class MainAgent {
|
|
|
55
55
|
}
|
|
56
56
|
/* ───────────── main run ───────────── */
|
|
57
57
|
async run() {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
try {
|
|
59
|
+
this.runCount = 0;
|
|
60
|
+
await this.runBoot();
|
|
61
|
+
await this.runScope();
|
|
62
|
+
await this.runGrounding();
|
|
63
|
+
await this.runWorkLoop();
|
|
64
|
+
await this.runFinalize();
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
this.ui.stop(); // ← guaranteed cleanup
|
|
68
|
+
}
|
|
64
69
|
}
|
|
65
70
|
/* ───────────── boot ───────────── */
|
|
66
71
|
async runBoot() {
|
|
@@ -86,31 +91,16 @@ export class MainAgent {
|
|
|
86
91
|
await scopeClassificationStep.run(this.context);
|
|
87
92
|
this.logLine("TASK", "Scope classification complete");
|
|
88
93
|
}
|
|
89
|
-
/* ───────────── grounding / info acquisition loop ───────────── */
|
|
90
94
|
async runGrounding() {
|
|
91
95
|
let ready = false;
|
|
92
|
-
while (
|
|
93
|
-
// ----------------
|
|
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 ----------------
|
|
96
|
+
while (this.runCount < this.maxRuns) {
|
|
97
|
+
// ---------------- EVIDENCE PIPELINE ----------------
|
|
106
98
|
const t1 = this.startTimer();
|
|
107
99
|
await evidenceVerifierStep.run({ query: this.query, context: this.context });
|
|
108
100
|
this.logLine("ANALYSIS", "collectAnalysisEvidence", t1());
|
|
109
|
-
/* ───────────── precheck ───────────── */
|
|
110
101
|
const t2 = this.startTimer();
|
|
111
102
|
await fileCheckStep(this.context);
|
|
112
103
|
this.logLine("ANALYSIS", "fileCheckStep", t2());
|
|
113
|
-
// Select grounded candidate files
|
|
114
104
|
const t3 = this.startTimer();
|
|
115
105
|
await selectRelevantSourcesStep.run({ query: this.query, context: this.context });
|
|
116
106
|
this.logLine("ANALYSIS", "selectRelevantSources", t3());
|
|
@@ -119,12 +109,29 @@ export class MainAgent {
|
|
|
119
109
|
await readinessGateStep.run(this.context);
|
|
120
110
|
this.logLine("HASINFO", "readinessGate", t4());
|
|
121
111
|
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 });
|
|
112
|
+
if (ready) {
|
|
113
|
+
break;
|
|
127
114
|
}
|
|
115
|
+
// ---------------- INFORMATION ACQUISITION ----------------
|
|
116
|
+
if (this.canExecutePhase("planning") &&
|
|
117
|
+
this.canExecuteScope("planning")) {
|
|
118
|
+
const t = this.startTimer();
|
|
119
|
+
await infoPlanGenStep.run(this.context);
|
|
120
|
+
const infoPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
|
|
121
|
+
// If we are about to execute a new info acquisition wave,
|
|
122
|
+
// wipe previous search results.
|
|
123
|
+
if (infoPlan.steps.length > 0 && this.context.initContext && this.context.analysis?.focus) {
|
|
124
|
+
this.context.initContext.relatedFiles = [];
|
|
125
|
+
this.context.analysis.focus.candidateFiles = [];
|
|
126
|
+
}
|
|
127
|
+
for (const step of infoPlan.steps) {
|
|
128
|
+
const stepIO = { query: this.query };
|
|
129
|
+
await this.executeStep(step, stepIO);
|
|
130
|
+
}
|
|
131
|
+
this.logLine("PLAN", "infoPlanGen", t(), undefined, { highlight: false });
|
|
132
|
+
}
|
|
133
|
+
this.runCount++;
|
|
134
|
+
this.logLine("HASINFO", "Not ready — looping back to evidence collection", undefined, undefined, { highlight: false });
|
|
128
135
|
}
|
|
129
136
|
}
|
|
130
137
|
/* ───────────── finalize ───────────── */
|
|
@@ -135,17 +142,43 @@ export class MainAgent {
|
|
|
135
142
|
}
|
|
136
143
|
/* ───────────── work loop ───────────── */
|
|
137
144
|
async runWorkLoop() {
|
|
145
|
+
const readinessDecision = this.context.analysis?.readiness?.decision;
|
|
146
|
+
const readinessConfidence = this.context.analysis?.readiness?.confidence ?? 0;
|
|
147
|
+
if (readinessDecision !== "ready") {
|
|
148
|
+
// ❌ Graceful fallback instead of throwing
|
|
149
|
+
this.context.task.status = "deferred";
|
|
150
|
+
this.context.task.reason = `Readiness not achieved (decision=${readinessDecision}, confidence=${readinessConfidence})`;
|
|
151
|
+
persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
|
|
152
|
+
this.logLine("TASK", `Cannot start work loop — agent needs more evidence to safely proceed`, undefined, `Readiness: ${readinessDecision}, Confidence: ${readinessConfidence}`, { highlight: true });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!this.context.task) {
|
|
156
|
+
throw new Error("runWorkLoop: missing task");
|
|
157
|
+
}
|
|
138
158
|
const MAX_TASK_STEPS = 5;
|
|
139
159
|
let stepCount = 0;
|
|
140
|
-
while (stepCount < MAX_TASK_STEPS
|
|
160
|
+
while (stepCount < MAX_TASK_STEPS &&
|
|
161
|
+
this.context.task.status === "active") {
|
|
141
162
|
await reasonNextTaskStep.run(this.context);
|
|
142
163
|
const nextAction = this.context.analysis?.iterationReasoning?.nextAction;
|
|
164
|
+
// 🟡 Pause for user clarification
|
|
165
|
+
if (nextAction === "request-feedback") {
|
|
166
|
+
this.context.task.status = "paused";
|
|
167
|
+
persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
|
|
168
|
+
this.logLine("TASK", "Execution paused — awaiting user clarification", undefined, undefined, { highlight: false });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// 🟢 Completed task
|
|
143
172
|
if (nextAction === "complete") {
|
|
173
|
+
this.context.task.status = "completed";
|
|
174
|
+
persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
|
|
144
175
|
this.logLine("TASK", "All selected files processed — task complete", undefined, undefined, { highlight: false });
|
|
145
176
|
return;
|
|
146
177
|
}
|
|
147
178
|
const taskStep = await iterationFileSelector.run(this.context);
|
|
148
179
|
if (!taskStep) {
|
|
180
|
+
this.context.task.status = "completed";
|
|
181
|
+
persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
|
|
149
182
|
this.logLine("TASK", "No eligible taskStep found — task complete", undefined, undefined, { highlight: false });
|
|
150
183
|
return;
|
|
151
184
|
}
|
|
@@ -155,11 +188,11 @@ export class MainAgent {
|
|
|
155
188
|
taskStep.stepIndex = stepCount;
|
|
156
189
|
taskStep.status = "pending";
|
|
157
190
|
persistTaskStepInsert(taskStep, getDbForRepo());
|
|
158
|
-
this.logLine("NEW STEP", `Processing taskStep ${stepCount}
|
|
191
|
+
this.logLine("NEW STEP", `Processing taskStep ${stepCount}`, undefined, taskStep.filePath, { highlight: true });
|
|
159
192
|
taskStep.startTime = Date.now();
|
|
160
193
|
persistTaskStepStart(taskStep, getDbForRepo());
|
|
161
194
|
// ---------------------------
|
|
162
|
-
// Step-level iterations
|
|
195
|
+
// Step-level iterations
|
|
163
196
|
// ---------------------------
|
|
164
197
|
const stepAction = await this.runStepIterations(taskStep);
|
|
165
198
|
if (stepAction === "complete") {
|
|
@@ -229,7 +229,19 @@ export const evidenceVerifierStep = {
|
|
|
229
229
|
query,
|
|
230
230
|
data: { fileAnalysis: context.analysis.fileAnalysis },
|
|
231
231
|
};
|
|
232
|
-
|
|
232
|
+
// Build compact log summary
|
|
233
|
+
const logSummary = Object.entries(context.analysis.fileAnalysis).map(([path, analysis]) => {
|
|
234
|
+
const evidenceCount = analysis.evidence?.length ?? 0;
|
|
235
|
+
const confidenceMatch = analysis.relevanceExplanation?.match(/\[confidence:(\d+\.\d+)\]/);
|
|
236
|
+
const confidence = confidenceMatch?.[1] ?? "0.00";
|
|
237
|
+
return {
|
|
238
|
+
file: path,
|
|
239
|
+
confidence,
|
|
240
|
+
evidenceCount,
|
|
241
|
+
isRelevant: analysis.action?.isRelevant ?? false,
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
logInputOutput("evidenceVerifier", "output", logSummary);
|
|
233
245
|
return output;
|
|
234
246
|
},
|
|
235
247
|
};
|
|
@@ -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
|
};
|
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
|
}
|
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
|
-
};
|