scai 0.1.166 → 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.
@@ -19,18 +19,9 @@ 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 chalk from "chalk";
22
23
  /* ───────────────────────── registry ───────────────────────── */
23
- /**
24
- * Registry mapping action names to their corresponding Module implementations.
25
- * This registry is populated with built-in modules from the module registry.
26
- */
27
24
  const MODULE_REGISTRY = Object.fromEntries(Object.entries(builtInModules).map(([name, mod]) => [name, mod]));
28
- /**
29
- * Resolves a module by its action name.
30
- *
31
- * @param action - The action name to resolve.
32
- * @returns The Module implementation if found, otherwise undefined.
33
- */
34
25
  function resolveModuleForAction(action) {
35
26
  return MODULE_REGISTRY[action];
36
27
  }
@@ -64,13 +55,17 @@ export class MainAgent {
64
55
  }
65
56
  /* ───────────── main run ───────────── */
66
57
  async run() {
67
- this.runCount = 0;
68
- await this.runBoot();
69
- await this.runPrecheck();
70
- await this.runScope();
71
- await this.runGrounding();
72
- await this.runWorkLoop();
73
- await this.runFinalize();
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
+ }
74
69
  }
75
70
  /* ───────────── boot ───────────── */
76
71
  async runBoot() {
@@ -78,128 +73,141 @@ export class MainAgent {
78
73
  await understandIntentStep.run({ context: this.context });
79
74
  await resolveExecutionModeStep.run(this.context);
80
75
  // Boot the task and get the real DB taskId
81
- this.taskId = bootTaskForRepo(this.context, getDbForRepo(), this.logLine.bind(this));
82
- // Ensure context.task exists and has all required fields
76
+ this.taskId = bootTaskForRepo(this.context, getDbForRepo(), (phase, step, ms, desc) => this.logLine(phase, step, ms, desc, { highlight: true }));
83
77
  (_a = this.context).task || (_a.task = {
84
78
  id: this.taskId,
85
- projectId: 0, // optionally set to a real projectId if available
79
+ projectId: 0,
86
80
  status: "active",
87
81
  initialQuery: this.context.initContext?.userQuery ?? "",
88
82
  createdAt: new Date().toISOString(),
89
83
  updatedAt: new Date().toISOString(),
90
84
  taskSteps: [],
91
85
  });
92
- // If task existed but id wasn’t set, set it now
93
86
  this.context.task.id = this.taskId;
94
- }
95
- /* ───────────── precheck ───────────── */
96
- async runPrecheck() {
97
- await fileCheckStep(this.context);
87
+ this.logLine("TASK", "Boot complete", undefined, `taskId=${this.taskId}`, { highlight: true });
98
88
  }
99
89
  /* ───────────── scope ───────────── */
100
90
  async runScope() {
101
91
  await scopeClassificationStep.run(this.context);
92
+ this.logLine("TASK", "Scope classification complete");
102
93
  }
103
- /* ───────────── grounding / info acquisition loop ───────────── */
104
94
  async runGrounding() {
105
95
  let ready = false;
106
- while (!ready && this.runCount < this.maxRuns) {
96
+ while (this.runCount < this.maxRuns) {
97
+ // ---------------- EVIDENCE PIPELINE ----------------
98
+ const t1 = this.startTimer();
99
+ await evidenceVerifierStep.run({ query: this.query, context: this.context });
100
+ this.logLine("ANALYSIS", "collectAnalysisEvidence", t1());
101
+ const t2 = this.startTimer();
102
+ await fileCheckStep(this.context);
103
+ this.logLine("ANALYSIS", "fileCheckStep", t2());
104
+ const t3 = this.startTimer();
105
+ await selectRelevantSourcesStep.run({ query: this.query, context: this.context });
106
+ this.logLine("ANALYSIS", "selectRelevantSources", t3());
107
+ // ---------------- READINESS GATE ----------------
108
+ const t4 = this.startTimer();
109
+ await readinessGateStep.run(this.context);
110
+ this.logLine("HASINFO", "readinessGate", t4());
111
+ ready = this.context.analysis?.readiness?.decision === "ready";
112
+ if (ready) {
113
+ break;
114
+ }
107
115
  // ---------------- INFORMATION ACQUISITION ----------------
108
116
  if (this.canExecutePhase("planning") &&
109
117
  this.canExecuteScope("planning")) {
110
118
  const t = this.startTimer();
111
- // Generate info plan step(s) in context
112
119
  await infoPlanGenStep.run(this.context);
113
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
+ }
114
127
  for (const step of infoPlan.steps) {
115
128
  const stepIO = { query: this.query };
116
129
  await this.executeStep(step, stepIO);
117
- // Re-check any new files discovered
118
- const t2 = this.startTimer();
119
- await fileCheckStep(this.context);
120
- this.logLine("PRECHECK", "postFileSearch", t2());
121
- }
122
- this.logLine("PLAN", "infoPlanGen", t());
123
- }
124
- // ---------------- DETERMINISTIC EVIDENCE VERIFICATION ----------------
125
- const t1 = this.startTimer();
126
- await evidenceVerifierStep.run({ query: this.query, context: this.context });
127
- this.logLine("ANALYSIS", "collectAnalysisEvidence", t1(), undefined);
128
- // Select grounded candidate files
129
- const t2 = this.startTimer();
130
- await selectRelevantSourcesStep.run({ query: this.query, context: this.context });
131
- this.logLine("ANALYSIS", "selectRelevantSources", t2(), undefined);
132
- // ---------------- READINESS GATE ----------------
133
- const t3 = this.startTimer();
134
- await readinessGateStep.run(this.context);
135
- this.logLine("HASINFO", "readinessGate", t3(), undefined);
136
- ready = this.context.analysis?.readiness?.decision === "ready";
137
- if (!ready) {
138
- this.runCount++;
139
- // Previously resetInitContextForLoop removed; optionally clear temporary data
140
- if (this.context.analysis) {
141
- this.context.analysis.planSuggestion = undefined;
142
130
  }
143
- this.logLine("HASINFO", "readinessGate", undefined, "Not ready, looping back to information acquisition");
131
+ this.logLine("PLAN", "infoPlanGen", t(), undefined, { highlight: false });
144
132
  }
133
+ this.runCount++;
134
+ this.logLine("HASINFO", "Not ready — looping back to evidence collection", undefined, undefined, { highlight: false });
145
135
  }
146
136
  }
147
137
  /* ───────────── finalize ───────────── */
148
138
  async runFinalize() {
149
- // generate final answer
150
139
  await finalAnswerModule.run({ query: this.query, context: this.context });
151
- // persist task and steps
152
140
  persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
141
+ this.logLine("TASK", "Finalize complete", undefined, undefined, { highlight: false });
153
142
  }
154
143
  /* ───────────── work loop ───────────── */
155
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
+ }
156
158
  const MAX_TASK_STEPS = 5;
157
159
  let stepCount = 0;
158
- while (stepCount < MAX_TASK_STEPS) {
160
+ while (stepCount < MAX_TASK_STEPS &&
161
+ this.context.task.status === "active") {
159
162
  await reasonNextTaskStep.run(this.context);
160
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
161
172
  if (nextAction === "complete") {
162
- this.logLine("TASK", "All selected files processed — task complete");
173
+ this.context.task.status = "completed";
174
+ persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
175
+ this.logLine("TASK", "All selected files processed — task complete", undefined, undefined, { highlight: false });
163
176
  return;
164
177
  }
165
178
  const taskStep = await iterationFileSelector.run(this.context);
166
179
  if (!taskStep) {
167
- this.logLine("TASK", "No eligible taskStep found — task complete");
180
+ this.context.task.status = "completed";
181
+ persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
182
+ this.logLine("TASK", "No eligible taskStep found — task complete", undefined, undefined, { highlight: false });
168
183
  return;
169
184
  }
170
- // Set current step in context
171
- if (this.context.task) {
172
- this.context.task.currentStep = taskStep;
173
- }
185
+ this.context.task.currentStep = taskStep;
174
186
  stepCount++;
175
187
  taskStep.taskId = this.taskId;
176
188
  taskStep.stepIndex = stepCount;
177
189
  taskStep.status = "pending";
178
- // ⬇️ Persist newly created step
179
190
  persistTaskStepInsert(taskStep, getDbForRepo());
180
- logInputOutput("Step to process", "input", taskStep);
181
- this.logLine("TASK", `Processing taskStep ${stepCount}/${MAX_TASK_STEPS}`, undefined, taskStep.filePath);
182
- // ---------------------------
183
- // Start the step
184
- // ---------------------------
191
+ this.logLine("NEW STEP", `Processing taskStep ${stepCount}`, undefined, taskStep.filePath, { highlight: true });
185
192
  taskStep.startTime = Date.now();
186
193
  persistTaskStepStart(taskStep, getDbForRepo());
187
194
  // ---------------------------
188
- // Step-level iterations: reason only about this file
195
+ // Step-level iterations
189
196
  // ---------------------------
190
197
  const stepAction = await this.runStepIterations(taskStep);
191
198
  if (stepAction === "complete") {
192
199
  taskStep.status = "completed";
193
200
  taskStep.endTime = Date.now();
194
201
  persistTaskStepCompletion(taskStep, getDbForRepo());
202
+ this.logLine("STEP-DONE", `Completed taskStep ${stepCount}`, undefined, taskStep.filePath, { highlight: false });
195
203
  }
196
204
  else {
197
- // optionally mark as pending or redo-step
198
205
  taskStep.status = "pending";
199
206
  persistTaskStepCompletion(taskStep, getDbForRepo());
207
+ this.logLine("STEP", `Pending taskStep ${stepCount}`, undefined, taskStep.filePath);
200
208
  }
201
209
  }
202
- this.logLine("TASK", "Max task step limit reached — stopping work loop");
210
+ this.logLine("TASK", "Max task step limit reached — stopping work loop", undefined, undefined, { highlight: false });
203
211
  }
204
212
  /* ───────────── step iterations ───────────── */
205
213
  async runStepIterations(taskStep) {
@@ -207,31 +215,22 @@ export class MainAgent {
207
215
  let loopCount = 0;
208
216
  const getNextIterationAction = () => {
209
217
  const nextAction = this.context.analysis?.iterationReasoning?.nextAction;
210
- if (nextAction !== "continue" &&
211
- nextAction !== "redo-step" &&
212
- nextAction !== "expand-scope" &&
213
- nextAction !== "request-feedback" &&
214
- nextAction !== "complete") {
215
- return "complete"; // fallback
216
- }
218
+ if (!["continue", "redo-step", "expand-scope", "request-feedback", "complete"].includes(nextAction ?? ""))
219
+ return "complete";
217
220
  return nextAction;
218
221
  };
219
- // Loop through iterations for a single task step (file)
220
222
  while (loopCount < MAX_ITERATIONS) {
221
223
  this.runCount++;
222
224
  loopCount++;
223
- // 🔴 HARD RESET — no carryover allowed
224
- if (this.context.analysis?.iterationReasoning) {
225
+ if (this.context.analysis?.iterationReasoning)
225
226
  this.context.analysis.iterationReasoning.nextAction = undefined;
226
- }
227
227
  await this.runWorkIteration(taskStep);
228
228
  const nextAction = getNextIterationAction();
229
- this.logLine("STEP-LOOP", "nextAction", undefined, nextAction);
229
+ this.logLine("STEP-LOOP", `nextAction = ${nextAction}`);
230
230
  if (nextAction === "complete")
231
231
  return "complete";
232
232
  if (nextAction === "request-feedback")
233
233
  return "request-feedback";
234
- // else: continue
235
234
  }
236
235
  return "continue";
237
236
  }
@@ -239,56 +238,47 @@ export class MainAgent {
239
238
  async runWorkIteration(taskStep) {
240
239
  if (!this.context.analysis)
241
240
  this.context.analysis = {};
242
- // ---------------- ANALYSIS ----------------
243
241
  if (this.canExecutePhase("analysis") && this.canExecuteScope("analysis")) {
244
242
  const tAnalysis = this.startTimer();
245
243
  await analysisPlanGenStep.run(this.context);
246
- this.logLine("PLAN", "analysisPlanGen", tAnalysis());
244
+ this.logLine("PLAN", "analysisPlanGen", tAnalysis(), undefined, { highlight: false });
247
245
  const analysisPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
248
246
  for (const step of analysisPlan.steps) {
249
247
  const tStep = this.startTimer();
250
248
  await this.executeStep(step, { query: this.query });
251
- this.logLine("STEP", step.action || "unnamedStep", tStep());
249
+ this.logLine("PLANNING-STEP", step.action || "unnamedStep", tStep());
252
250
  }
253
- if (this.context.analysis) {
251
+ if (this.context.analysis)
254
252
  this.context.analysis.planSuggestion = undefined;
255
- }
256
253
  }
257
- // ---------------- TRANSFORM ----------------
258
254
  if (this.canExecutePhase("transform") && this.canExecuteScope("transform")) {
259
255
  const tTransform = this.startTimer();
260
256
  await transformPlanGenStep.run(this.context);
261
- this.logLine("PLAN", "transformPlanGen", tTransform());
257
+ this.logLine("PLAN", "transformPlanGen", tTransform(), undefined, { highlight: false });
262
258
  const transformPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
263
259
  const firstStep = transformPlan.steps[0];
264
260
  if (firstStep) {
265
261
  const tStep = this.startTimer();
266
262
  await this.executeStep(firstStep, { query: this.query });
267
- this.logLine("STEP", `#1 (only) - ${firstStep.action || "unnamedStep"}`, tStep());
263
+ this.logLine("PLANNING-STEP", `#1 (only) - ${firstStep.action || "unnamedStep"}`, tStep());
268
264
  }
269
- if (this.context.analysis) {
265
+ if (this.context.analysis)
270
266
  this.context.analysis.planSuggestion = undefined;
271
- }
272
- // ---------------- WRITE ----------------
273
267
  if (this.canExecutePhase("write") && this.canExecuteScope("write")) {
274
268
  const tWrite = this.startTimer();
275
269
  await writeFileStep.run({ query: this.query, context: this.context });
276
270
  this.logLine("WRITE", "writeFileStep", tWrite());
277
271
  }
278
- // ---------------- VALIDATION ----------------
279
272
  const tValidate = this.startTimer();
280
273
  await validateChangesStep.run(this.context);
281
274
  this.logLine("VALIDATION", "validateChangesStep", tValidate());
282
275
  }
283
- // ---------------- REASONING ----------------
284
276
  const tReason = this.startTimer();
285
277
  await reasonNextStep.run(this.context, taskStep);
286
278
  this.logLine("REASONING", "reasonNextStep", tReason());
287
- // ---------------- COLLABORATOR FEEDBACK ----------------
288
279
  const tCollab = this.startTimer();
289
280
  await collaboratorStep.run(this.context);
290
281
  this.logLine("FEEDBACK", "collaboratorStep", tCollab());
291
- // ---------------- INTEGRATE FEEDBACK ----------------
292
282
  const tIntegrate = this.startTimer();
293
283
  await integrateFeedbackStep.run(this.context);
294
284
  this.logLine("FEEDBACK", "integrateFeedbackStep", tIntegrate());
@@ -304,9 +294,7 @@ export class MainAgent {
304
294
  */
305
295
  async executeStep(step, input) {
306
296
  const stop = this.startTimer();
307
- // Set current step in context
308
297
  this.context.currentStep = step;
309
- // Resolve module for this action
310
298
  const mod = resolveModuleForAction(step.action);
311
299
  if (!mod) {
312
300
  this.logLine("EXECUTE", step.action, stop(), "skipped (missing module)");
@@ -314,13 +302,7 @@ export class MainAgent {
314
302
  }
315
303
  try {
316
304
  this.ui.update(`Running step: ${step.action}`);
317
- // Execute the module
318
- await mod.run({
319
- query: step.description ?? input.query,
320
- content: input.data ?? input.content,
321
- context: this.context
322
- });
323
- // Return ModuleIO (can remain minimal since output is untyped)
305
+ await mod.run({ query: step.description ?? input.query, content: input.data ?? input.content, context: this.context });
324
306
  return { query: step.description ?? input.query, data: {} };
325
307
  }
326
308
  catch (err) {
@@ -342,12 +324,10 @@ export class MainAgent {
342
324
  switch (phase) {
343
325
  case "analysis":
344
326
  case "planning":
345
- // Read-only phases are always allowed
346
327
  allowed = !docsOnly;
347
328
  break;
348
329
  case "transform":
349
330
  case "write":
350
- // Side-effect phases require explicit permission
351
331
  allowed = constraints?.allowFileWrites ?? false;
352
332
  break;
353
333
  }
@@ -365,14 +345,10 @@ export class MainAgent {
365
345
  let allowed = false;
366
346
  switch (scope) {
367
347
  case "none":
368
- // Non-repo questions: analysis/planning only
369
348
  allowed = phase === "analysis" || phase === "planning";
370
349
  break;
371
- case "single-file":
372
- case "multi-file":
373
- case "repo-wide":
350
+ default:
374
351
  allowed = true;
375
- break;
376
352
  }
377
353
  return allowed;
378
354
  }
@@ -396,38 +372,33 @@ export class MainAgent {
396
372
  * @param fn - The function to execute while spinner is paused.
397
373
  */
398
374
  withSpinnerPaused(fn) {
399
- this.ui.pause(() => {
400
- // Ensure spinner line is fully gone
401
- process.stdout.write('\r\x1b[K');
402
- fn();
403
- });
375
+ this.ui.pause(() => { process.stdout.write('\r\x1b[K'); fn(); });
404
376
  }
405
- /**
406
- * Logs a line with timing information.
407
- *
408
- * @param phase - The phase of execution.
409
- * @param step - The step being executed.
410
- * @param ms - The execution time in milliseconds.
411
- * @param desc - Optional description of the step.
412
- */
413
- logLine(phase, step, ms, desc) {
377
+ logLine(phase, step, ms, desc, options) {
414
378
  this.withSpinnerPaused(() => {
415
379
  const suffix = desc ? ` — ${desc}` : "";
416
380
  const timing = typeof ms === "number" ? ` (${ms}ms)` : "";
417
- console.log(`[AGENT] ${phase} :: ${step}${suffix}${timing}`);
381
+ let line = `[AGENT] ${phase} :: ${step}${suffix}${timing}`;
382
+ // Coloring rules
383
+ if (phase === "NEW STEP" || phase === "STEP") {
384
+ line = chalk.green(line);
385
+ }
386
+ else if (options?.highlight) {
387
+ line = chalk.green(line);
388
+ }
389
+ // Print a blank line **before** NEW STEP only
390
+ if (phase === "NEW STEP")
391
+ console.log();
392
+ console.log(line);
418
393
  });
419
394
  }
420
- /**
421
- * Logs a message to the user output.
422
- *
423
- * @param message - The message to log.
424
- */
425
395
  userOutput(message) {
426
396
  this.withSpinnerPaused(() => {
427
397
  console.log(`[USER OUTPUT] ${message}`);
428
398
  });
429
399
  }
430
400
  }
401
+ // All helper functions (persistTaskData, bootTaskForRepo, persistTaskStep*) remain unchanged
431
402
  /* ───────────── FOLDER CAPSULES SUMMARY HELPER ───────────── */
432
403
  /**
433
404
  * Generates a human-readable summary of folder capsules and logs it.