minovative-mind-cli 1.5.1 → 2.0.0
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/README.md +51 -45
- package/dist/commands/chat.js +7 -2
- package/dist/services/agent/slashCommands.js +156 -30
- package/dist/services/agent/toolLoop.d.ts +1 -1
- package/dist/services/agent/toolLoop.js +7 -2
- package/dist/services/agent/types.d.ts +2 -0
- package/dist/services/agent-tools.d.ts +9 -4
- package/dist/services/agent-tools.js +145 -21
- package/dist/services/agent.d.ts +8 -0
- package/dist/services/agent.js +285 -40
- package/dist/services/ai.d.ts +19 -5
- package/dist/services/ai.js +167 -35
- package/dist/services/changeLogger.d.ts +142 -0
- package/dist/services/changeLogger.js +132 -3
- package/dist/services/contextAgent.d.ts +6 -1
- package/dist/services/contextAgent.js +95 -14
- package/dist/services/embeddingIndex.d.ts +82 -0
- package/dist/services/embeddingIndex.js +613 -0
- package/dist/services/investigationComplexity.d.ts +45 -0
- package/dist/services/investigationComplexity.js +91 -0
- package/dist/services/metrics.d.ts +18 -0
- package/dist/services/metrics.js +7 -0
- package/dist/services/orchestration/fileLockRegistry.d.ts +125 -0
- package/dist/services/orchestration/fileLockRegistry.js +276 -0
- package/dist/services/orchestration/investigationAgent.d.ts +85 -0
- package/dist/services/orchestration/investigationAgent.js +359 -0
- package/dist/services/orchestration/investigationOrchestrator.d.ts +53 -0
- package/dist/services/orchestration/investigationOrchestrator.js +180 -0
- package/dist/services/orchestration/messageBus.d.ts +162 -0
- package/dist/services/orchestration/messageBus.js +225 -0
- package/dist/services/orchestration/orchestrator.d.ts +45 -0
- package/dist/services/orchestration/orchestrator.js +214 -0
- package/dist/services/orchestration/readCache.d.ts +79 -0
- package/dist/services/orchestration/readCache.js +108 -0
- package/dist/services/orchestration/scopedTools.d.ts +57 -0
- package/dist/services/orchestration/scopedTools.js +172 -0
- package/dist/services/orchestration/subAgent.d.ts +58 -0
- package/dist/services/orchestration/subAgent.js +187 -0
- package/dist/services/orchestration/taskGraph.d.ts +129 -0
- package/dist/services/orchestration/taskGraph.js +254 -0
- package/dist/services/proxyClient.d.ts +25 -0
- package/dist/services/proxyClient.js +60 -0
- package/dist/utils/asyncContext.d.ts +16 -0
- package/dist/utils/asyncContext.js +25 -0
- package/dist/utils/config.d.ts +3 -1
- package/dist/utils/config.js +3 -1
- package/dist/utils/contextPrompts.js +3 -2
- package/dist/utils/dependencyTracer/modules/api.d.ts +9 -0
- package/dist/utils/dependencyTracer/modules/api.js +62 -0
- package/dist/utils/dependencyTracer/modules/graph.d.ts +9 -0
- package/dist/utils/dependencyTracer/modules/graph.js +23 -0
- package/dist/utils/dependencyTracer/modules/profiles.d.ts +7 -0
- package/dist/utils/dependencyTracer/modules/profiles.js +120 -0
- package/dist/utils/dependencyTracer/modules/resolver.d.ts +7 -0
- package/dist/utils/dependencyTracer/modules/resolver.js +51 -0
- package/dist/utils/dependencyTracer/modules/types.d.ts +4 -0
- package/dist/utils/dependencyTracer/modules/types.js +1 -0
- package/dist/utils/dependencyTracer/modules/walker.d.ts +1 -0
- package/dist/utils/dependencyTracer/modules/walker.js +48 -0
- package/dist/utils/dependencyTracer.js +31 -17
- package/dist/utils/excludedExtensions.js +0 -1
- package/dist/utils/historyPrompt.d.ts +9 -0
- package/dist/utils/historyPrompt.js +87 -0
- package/dist/utils/logo.js +7 -7
- package/dist/utils/paste.d.ts +21 -0
- package/dist/utils/paste.js +22 -1
- package/dist/utils/profiles.d.ts +2 -0
- package/dist/utils/profiles.js +44 -0
- package/dist/utils/projectStorage.js +10 -7
- package/dist/utils/systemPrompts.d.ts +6 -3
- package/dist/utils/systemPrompts.js +106 -5
- package/dist/utils/types.d.ts +33 -0
- package/dist/utils/types.js +1 -0
- package/oclif.manifest.json +2 -2
- package/package.json +4 -3
package/dist/services/agent.js
CHANGED
|
@@ -23,17 +23,23 @@ import { marked } from 'marked';
|
|
|
23
23
|
import { markedTerminal } from 'marked-terminal';
|
|
24
24
|
import { debugLog, isDebugOn } from '../utils/logger.js';
|
|
25
25
|
import { ensureProjectStorage, ensureIgnored, readCache, writeCache, invalidateCacheForDependents, } from '../utils/projectStorage.js';
|
|
26
|
-
import {
|
|
26
|
+
import { GEMINI_MODELS } from '../utils/config.js';
|
|
27
|
+
import { createSharedChatSession, getGeneralChatConfig, getPlanExecutionConfig, compressTextUsingFlashLite, generateChatTitle, getGlobalActiveModel, getGlobalLatestUsageMetadata, } from './ai.js';
|
|
27
28
|
import { changeLogger } from './changeLogger.js';
|
|
28
29
|
import { chatHistoryService } from './chatHistoryService.js';
|
|
29
|
-
import { gatherContext, routeIntent } from './contextAgent.js';
|
|
30
|
+
import { gatherContext, routeIntent, evaluateExecutionComplexity } from './contextAgent.js';
|
|
30
31
|
import { verifyChangedFiles } from './verificationService.js';
|
|
31
32
|
import { buildContextInjection } from '../utils/contextPrompts.js';
|
|
33
|
+
import { historyText } from '../utils/historyPrompt.js';
|
|
32
34
|
// Submodule Imports
|
|
33
35
|
import { AsyncInputHandler } from './agent/inputHandler.js';
|
|
34
36
|
import { processResponse } from './agent/toolLoop.js';
|
|
35
37
|
import { handleSlashCommand } from './agent/slashCommands.js';
|
|
36
|
-
|
|
38
|
+
import { getMetricCollector } from './metrics.js';
|
|
39
|
+
import { Orchestrator } from './orchestration/orchestrator.js';
|
|
40
|
+
import { isSubAgentsEnabled } from './agent-tools.js';
|
|
41
|
+
import { runWithAgentId } from '../utils/asyncContext.js';
|
|
42
|
+
marked.use(markedTerminal({ reflowText: false }));
|
|
37
43
|
// Export submodules for potential external uses if required
|
|
38
44
|
export { AsyncInputHandler } from './agent/inputHandler.js';
|
|
39
45
|
/**
|
|
@@ -77,7 +83,9 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
77
83
|
const chat = createSharedChatSession();
|
|
78
84
|
const inputHandler = new AsyncInputHandler();
|
|
79
85
|
const chatSessionState = { id: crypto.randomUUID(), title: '' };
|
|
86
|
+
const sessionInputHistory = [];
|
|
80
87
|
let isRawPasteMode = false;
|
|
88
|
+
let isPlanMode = false;
|
|
81
89
|
// Hook process.stdin.emit to intercept fast stream inputs.
|
|
82
90
|
// When large buffers containing newlines arrive rapidly, we interpret them as a clipboard paste,
|
|
83
91
|
// sanitizing and collapsing them to prevent premature command line submission.
|
|
@@ -102,7 +110,7 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
102
110
|
}
|
|
103
111
|
inputHandler.stop();
|
|
104
112
|
// Input collection (delegated to a helper function to avoid nested loop warning)
|
|
105
|
-
const { userInput: rawInput, canceled } = await collectUserInput();
|
|
113
|
+
const { userInput: rawInput, canceled } = await collectUserInput(sessionInputHistory, isPlanMode);
|
|
106
114
|
if (canceled) {
|
|
107
115
|
p.outro(pc.green('Goodbye! Happy coding. 🚀'));
|
|
108
116
|
return;
|
|
@@ -111,16 +119,28 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
111
119
|
if (!userInput) {
|
|
112
120
|
continue;
|
|
113
121
|
}
|
|
122
|
+
// Push non-empty input to history if it's not the same as the last item
|
|
123
|
+
if (sessionInputHistory[sessionInputHistory.length - 1] !== userInput) {
|
|
124
|
+
sessionInputHistory.push(userInput);
|
|
125
|
+
}
|
|
114
126
|
// Capture standalone forward slash triggers to open the selection console
|
|
115
127
|
if (userInput === '/') {
|
|
116
128
|
const commandMenu = await p['select']({
|
|
117
129
|
message: 'Command Menu',
|
|
118
130
|
options: [
|
|
119
131
|
{ value: '/models', label: '/models', hint: 'Change the active AI model' },
|
|
132
|
+
{ value: '/plan', label: '/plan', hint: 'Toggle plan mode (build a plan without executing)' },
|
|
120
133
|
{ value: '/paste', label: '/paste', hint: 'Paste large text directly into the CLI (Press Ctrl+D to submit)' },
|
|
121
134
|
{ value: '/clear', label: '/clear', hint: 'Clear chat session history' },
|
|
122
135
|
{ value: '/debug', label: '/debug', hint: 'Toggle internal debug logs' },
|
|
123
136
|
{ value: '/auto-approve', label: '/auto-approve', hint: 'Approve all future terminal commands' },
|
|
137
|
+
{ value: '/sub-agents', label: '/sub-agents', hint: 'Toggle the MMAAK Engine for parallel investigation and execution' },
|
|
138
|
+
{
|
|
139
|
+
value: '/semantic-search',
|
|
140
|
+
label: '/semantic-search',
|
|
141
|
+
hint: 'Toggle local vector index capabilities for better search',
|
|
142
|
+
},
|
|
143
|
+
{ value: '/stats', label: '/stats', hint: 'View current session statistics and configuration' },
|
|
124
144
|
{ value: '/commit', label: '/commit', hint: 'Auto-commit changes with AI message' },
|
|
125
145
|
{ value: '/revert', label: '/revert', hint: 'Undo last change' },
|
|
126
146
|
{ value: '/chats', label: '/chats', hint: 'View or resume past chat sessions' },
|
|
@@ -151,12 +171,16 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
151
171
|
workspaceRoot,
|
|
152
172
|
version,
|
|
153
173
|
isRawPasteMode,
|
|
174
|
+
isPlanMode,
|
|
154
175
|
chatSessionState,
|
|
155
176
|
};
|
|
156
177
|
const slashResult = await handleSlashCommand(userInput, slashCtx);
|
|
157
178
|
if (slashResult.isRawPasteMode !== undefined) {
|
|
158
179
|
isRawPasteMode = slashResult.isRawPasteMode;
|
|
159
180
|
}
|
|
181
|
+
if (slashResult.isPlanModeOverride !== undefined) {
|
|
182
|
+
isPlanMode = slashResult.isPlanModeOverride;
|
|
183
|
+
}
|
|
160
184
|
if (slashResult.userInputOverride !== undefined) {
|
|
161
185
|
userInput = slashResult.userInputOverride;
|
|
162
186
|
}
|
|
@@ -165,6 +189,36 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
165
189
|
}
|
|
166
190
|
}
|
|
167
191
|
// ─── Main Execution Cycle ────────────────────────────────────────
|
|
192
|
+
let currentInput = userInput;
|
|
193
|
+
let cachedContextResult = null;
|
|
194
|
+
while (true) {
|
|
195
|
+
const execResult = await executeSingleTurn(workspaceRoot, currentInput, chat, inputHandler, chatSessionState, isPlanMode, cachedContextResult);
|
|
196
|
+
const planModeReturn = execResult?.planModeReturn;
|
|
197
|
+
const contextResult = execResult?.contextResult;
|
|
198
|
+
if (isPlanMode && planModeReturn) {
|
|
199
|
+
if (planModeReturn === 'proceed') {
|
|
200
|
+
isPlanMode = false;
|
|
201
|
+
currentInput = 'Please execute the plan we just finalized.';
|
|
202
|
+
cachedContextResult = contextResult;
|
|
203
|
+
p.log.info(pc.cyan('Proceeding with execution...'));
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
else if (planModeReturn === 'edit') {
|
|
207
|
+
p.log.info(pc.cyan('Please provide additional instructions to revise the plan.'));
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
else if (planModeReturn === 'exit') {
|
|
211
|
+
isPlanMode = false;
|
|
212
|
+
p.log.info(pc.cyan('Exited plan mode. Returning to normal chat.'));
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHandler, chatSessionState, isPlanMode, cachedContextResult) {
|
|
221
|
+
return runWithAgentId('main', async () => {
|
|
168
222
|
const turnStartTime = Date.now();
|
|
169
223
|
const spinner = p.spinner();
|
|
170
224
|
const ac = new AbortController();
|
|
@@ -175,12 +229,20 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
175
229
|
let finalInput = userInput;
|
|
176
230
|
// Stage 1: Gather Workspace Context and route intentions
|
|
177
231
|
spinner.start('🔍 Investigating workspace...');
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
232
|
+
let gatherRes = { targetAgent: 'EXECUTE', chainedMessages: [], contextResult: cachedContextResult || null };
|
|
233
|
+
if (!cachedContextResult) {
|
|
234
|
+
const collector = getMetricCollector();
|
|
235
|
+
if (collector)
|
|
236
|
+
collector.startTimer('contextGather');
|
|
237
|
+
const chatHistory = chat.getRecentHistory(3);
|
|
238
|
+
gatherRes = await gatherContext(workspaceRoot, userInput, chatHistory, inputHandler, ac.signal, (msg) => {
|
|
239
|
+
if (!inputHandler.isCurrentlyPrompting()) {
|
|
240
|
+
spinner.message(`🔍 Investigating workspace... ${pc.dim(msg)}`);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
if (collector)
|
|
244
|
+
collector.stopTimer('contextGather');
|
|
245
|
+
}
|
|
184
246
|
let latestUsage = undefined;
|
|
185
247
|
// Collect any inputs that were queued while the Context Agent was investigating
|
|
186
248
|
const leftoverMsg = inputHandler.getAndClear();
|
|
@@ -213,26 +275,64 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
213
275
|
}
|
|
214
276
|
// Hot-swap the underlying LLM system instruction context depending on intent (conversational vs execution)
|
|
215
277
|
debugLog(`Intent Router output: original targetAgent = ${gatherRes.targetAgent}, effective = ${effectiveTargetAgent}`);
|
|
216
|
-
|
|
278
|
+
let config;
|
|
279
|
+
if (isPlanMode) {
|
|
280
|
+
const { getPlanModeConfig } = await import('./ai.js');
|
|
281
|
+
config = getPlanModeConfig();
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
config = effectiveTargetAgent === 'CHAT' ? getGeneralChatConfig() : getPlanExecutionConfig();
|
|
285
|
+
}
|
|
217
286
|
let dynamicSystemInstruction = config.systemInstruction;
|
|
218
287
|
// Inject gathered workspace directories, dependency configurations, and matching search patterns
|
|
219
288
|
if (gatherRes.contextResult) {
|
|
220
289
|
// Compress each relevant file individually using helper to avoid nested loop warning
|
|
221
|
-
|
|
290
|
+
if (!cachedContextResult) {
|
|
291
|
+
gatherRes.contextResult.relevantFiles = await compressContextFiles(workspaceRoot, gatherRes.contextResult);
|
|
292
|
+
}
|
|
222
293
|
// Assemble the final context injection string
|
|
223
294
|
const contextInjection = buildContextInjection(gatherRes.contextResult);
|
|
224
295
|
debugLog(`Final compressed context injection size: ${contextInjection.length} chars`);
|
|
296
|
+
const collector = getMetricCollector();
|
|
297
|
+
if (collector)
|
|
298
|
+
collector.recordCompressedContextSize(contextInjection.length);
|
|
225
299
|
dynamicSystemInstruction += '\n\n' + contextInjection;
|
|
226
300
|
}
|
|
227
301
|
// Apply the dynamic prompt updates and tool registrations to the active chat session
|
|
228
302
|
chat.setAgentConfig(dynamicSystemInstruction, config.tools);
|
|
303
|
+
if (getGlobalActiveModel() === GEMINI_MODELS.AUTO) {
|
|
304
|
+
let selectedModel = GEMINI_MODELS.FLASH_3_5;
|
|
305
|
+
if (effectiveTargetAgent === 'CHAT') {
|
|
306
|
+
selectedModel = GEMINI_MODELS.FLASH_LITE_3_1;
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
spinner.start(pc.blue('🧠 Evaluating execution complexity...'));
|
|
310
|
+
const complexity = await evaluateExecutionComplexity(finalInput, gatherRes.contextResult?.summary, gatherRes.contextResult?.relevantFiles?.size || 0);
|
|
311
|
+
spinner.stop();
|
|
312
|
+
process.stdout.write('\x1b[2K\r');
|
|
313
|
+
selectedModel = complexity === 'EASY' ? GEMINI_MODELS.FLASH_LITE_3_1 : GEMINI_MODELS.FLASH_3_5;
|
|
314
|
+
}
|
|
315
|
+
chat.setModel(selectedModel);
|
|
316
|
+
debugLog(`Auto-routing to model ${selectedModel} based on intent ${effectiveTargetAgent}`);
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
chat.setModel(getGlobalActiveModel());
|
|
320
|
+
}
|
|
229
321
|
if (gatherRes.contextResult) {
|
|
230
|
-
if (!
|
|
231
|
-
|
|
232
|
-
|
|
322
|
+
if (!gatherRes.contextResult.isParallel) {
|
|
323
|
+
if (!inputHandler.isCurrentlyPrompting()) {
|
|
324
|
+
spinner.stop(pc.green('🔍 Investigation completed.'));
|
|
325
|
+
spinner.start('Thinking...');
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
p.log.success(pc.green('🔍 Investigation completed.'));
|
|
329
|
+
}
|
|
233
330
|
}
|
|
234
331
|
else {
|
|
235
|
-
|
|
332
|
+
// Parallel investigation already printed its own detailed summary log
|
|
333
|
+
if (!inputHandler.isCurrentlyPrompting()) {
|
|
334
|
+
spinner.start('Thinking...');
|
|
335
|
+
}
|
|
236
336
|
}
|
|
237
337
|
}
|
|
238
338
|
else {
|
|
@@ -240,27 +340,106 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
240
340
|
spinner.message('Thinking...');
|
|
241
341
|
}
|
|
242
342
|
}
|
|
343
|
+
if (effectiveTargetAgent === 'EXECUTE' && isSubAgentsEnabled()) {
|
|
344
|
+
spinner.stop(); // Clear the spinner before delegating
|
|
345
|
+
process.stdout.write('\x1b[2K\r');
|
|
346
|
+
const orchestrator = new Orchestrator(workspaceRoot, chatSessionState.id, inputHandler);
|
|
347
|
+
const handledByOrchestrator = await orchestrator.runOrchestration(finalInput, dynamicSystemInstruction, ac.signal);
|
|
348
|
+
if (typeof handledByOrchestrator === 'string') {
|
|
349
|
+
// The orchestrator successfully decomposed and ran the task.
|
|
350
|
+
// Inject the task context into the main proxy chat session so the agent remembers what happened
|
|
351
|
+
// and so the history state gets saved properly on disk.
|
|
352
|
+
chat.getRawHistory().push({
|
|
353
|
+
role: 'user',
|
|
354
|
+
parts: [{ text: userInput }],
|
|
355
|
+
});
|
|
356
|
+
chat.getRawHistory().push({
|
|
357
|
+
role: 'model',
|
|
358
|
+
parts: [{ text: handledByOrchestrator }],
|
|
359
|
+
});
|
|
360
|
+
// Print the summary text just like single-agent mode
|
|
361
|
+
if (handledByOrchestrator !== '[Generation stopped.]') {
|
|
362
|
+
console.log(`\n${pc.blue('◆')} ${pc.bold('Minovative Mind')} ${pc.dim(`(Orchestrator)`)}\n`);
|
|
363
|
+
const cleanText = handledByOrchestrator.replace(/\n([ \t]*\n){2,}/g, '\n\n');
|
|
364
|
+
console.log(marked.parse(cleanText));
|
|
365
|
+
}
|
|
366
|
+
// Print Usage Stats
|
|
367
|
+
const usage = getGlobalLatestUsageMetadata();
|
|
368
|
+
if (usage) {
|
|
369
|
+
if (usage.cachedTokens > 0) {
|
|
370
|
+
const totalInputTokens = (usage.promptTokens || 0) + usage.cachedTokens;
|
|
371
|
+
const percentSaved = totalInputTokens > 0 ? Math.round((usage.cachedTokens / totalInputTokens) * 100) : 0;
|
|
372
|
+
p.log.info(`${pc.green('⚡')} ${pc.green('Context Cache Hit:')} ${pc.bold(usage.cachedTokens.toLocaleString())} tokens cached ${pc.dim(`(Saved ~${percentSaved}% of input cost)`)}`);
|
|
373
|
+
}
|
|
374
|
+
if (usage.remainingBalance !== undefined) {
|
|
375
|
+
p.log.info(`${pc.dim('Credits Remaining:')} ${pc.cyan(usage.remainingBalance.toLocaleString())}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Post-execution steps for the UI (saving history, invalidate cache)
|
|
379
|
+
const postExecutionChanges = changeLogger.getCurrentChangeSet()?.changes || [];
|
|
380
|
+
const modifiedFiles = postExecutionChanges
|
|
381
|
+
.filter((c) => c.action === 'modify' || c.action === 'create' || c.action === 'delete')
|
|
382
|
+
.map((c) => path.relative(workspaceRoot, c.filePath));
|
|
383
|
+
if (modifiedFiles.length > 0) {
|
|
384
|
+
await invalidateCacheForDependents(workspaceRoot, modifiedFiles);
|
|
385
|
+
}
|
|
386
|
+
const turnEndTime = Date.now();
|
|
387
|
+
const turnDuration = ((turnEndTime - turnStartTime) / 1000).toFixed(1);
|
|
388
|
+
p.log.info(`${pc.dim('Orchestration finished in')} ${pc.cyan(turnDuration + 's')}`);
|
|
389
|
+
changeLogger.markComplete();
|
|
390
|
+
changeLogger.commitChangeSet();
|
|
391
|
+
// Auto-save chat history for the orchestrator turn
|
|
392
|
+
const history = chat.getRawHistory();
|
|
393
|
+
if (history.length > 0) {
|
|
394
|
+
if (!chatSessionState.title) {
|
|
395
|
+
chatSessionState.title = 'Generating title...';
|
|
396
|
+
try {
|
|
397
|
+
const title = await generateChatTitle(userInput);
|
|
398
|
+
chatSessionState.title = title;
|
|
399
|
+
}
|
|
400
|
+
catch (e) {
|
|
401
|
+
debugLog('Failed to save session with title: ' + e);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
chatHistoryService
|
|
405
|
+
.saveSession({
|
|
406
|
+
id: chatSessionState.id,
|
|
407
|
+
title: chatSessionState.title,
|
|
408
|
+
timestamp: Date.now(),
|
|
409
|
+
history,
|
|
410
|
+
})
|
|
411
|
+
.catch((e) => debugLog('Failed to auto-save session: ' + e));
|
|
412
|
+
}
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
// If it returned false, the graph was too simple (1 task) or failed, so we fall through to the single-agent loop.
|
|
416
|
+
if (!inputHandler.isCurrentlyPrompting()) {
|
|
417
|
+
spinner.start('Thinking (Single-Agent)...');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
243
420
|
// Send the formulated prompt payload to the generative model
|
|
244
421
|
let result;
|
|
245
422
|
try {
|
|
246
423
|
result = await chat.sendMessage(finalInput, undefined, ac.signal);
|
|
247
424
|
}
|
|
248
425
|
catch (e) {
|
|
249
|
-
spinner.stop(
|
|
426
|
+
spinner.stop();
|
|
427
|
+
process.stdout.write('\x1b[2K\r');
|
|
250
428
|
if (e.name === 'AbortError' || e.message?.includes('abort')) {
|
|
251
429
|
p.log.warn(pc.yellow('Generation aborted by user.'));
|
|
252
430
|
}
|
|
253
431
|
else {
|
|
254
432
|
p.log.error(pc.red(`Error communicating with AI: ${e.message || String(e)}`));
|
|
255
433
|
}
|
|
256
|
-
|
|
434
|
+
return;
|
|
257
435
|
}
|
|
258
436
|
latestUsage = result.response.usageMetadata?.();
|
|
259
437
|
const grounding = result.response.groundingMetadata?.();
|
|
260
438
|
if (grounding?.webSearchQueries && grounding.webSearchQueries.length > 0) {
|
|
261
439
|
p.log.info(`${pc.blue('🌐')} ${pc.dim('Google Search Queries:')} ${grounding.webSearchQueries.map((q) => pc.cyan(`"${q}"`)).join(', ')}`);
|
|
262
440
|
}
|
|
263
|
-
spinner.stop(
|
|
441
|
+
spinner.stop();
|
|
442
|
+
process.stdout.write('\x1b[2K\r');
|
|
264
443
|
const calls = result.response.functionCalls();
|
|
265
444
|
debugLog(`Initial functionCalls: ${calls && calls.length > 0 ? JSON.stringify(calls) : 'None'}`);
|
|
266
445
|
// Stage 2: Recursive Tool Loops and Automated Self-Correction (delegated to a helper function to avoid nested loop warning)
|
|
@@ -303,44 +482,95 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
303
482
|
if (history.length > 0) {
|
|
304
483
|
if (!chatSessionState.title) {
|
|
305
484
|
chatSessionState.title = 'Generating title...';
|
|
306
|
-
|
|
485
|
+
try {
|
|
486
|
+
const title = await generateChatTitle(userInput);
|
|
307
487
|
chatSessionState.title = title;
|
|
308
|
-
chatHistoryService.saveSession({
|
|
488
|
+
await chatHistoryService.saveSession({
|
|
309
489
|
id: chatSessionState.id,
|
|
310
490
|
title: chatSessionState.title,
|
|
311
491
|
timestamp: Date.now(),
|
|
312
|
-
history: chat.getRawHistory()
|
|
313
|
-
})
|
|
314
|
-
}
|
|
492
|
+
history: chat.getRawHistory(),
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
catch (e) {
|
|
496
|
+
debugLog('Failed to save session with title: ' + e);
|
|
497
|
+
}
|
|
315
498
|
}
|
|
316
|
-
chatHistoryService
|
|
499
|
+
chatHistoryService
|
|
500
|
+
.saveSession({
|
|
317
501
|
id: chatSessionState.id,
|
|
318
502
|
title: chatSessionState.title,
|
|
319
503
|
timestamp: Date.now(),
|
|
320
|
-
history
|
|
321
|
-
})
|
|
504
|
+
history,
|
|
505
|
+
})
|
|
506
|
+
.catch((e) => debugLog('Failed to auto-save session: ' + e));
|
|
507
|
+
}
|
|
508
|
+
let planModeReturn = undefined;
|
|
509
|
+
if (isPlanMode && finalText && finalText !== '[Generation stopped.]') {
|
|
510
|
+
inputHandler.stop(); // MUST stop the async input handler before showing an interactive prompt, or it will intercept stdin and freeze/duplicate
|
|
511
|
+
// Ensure the terminal has fully scrolled and there is a clean line break
|
|
512
|
+
console.log('\n');
|
|
513
|
+
// Small delay to allow stdout to flush and terminal to finish scrolling from the markdown output
|
|
514
|
+
// This prevents @clack/prompts from miscalculating the cursor position and duplicating the prompt on arrow keys
|
|
515
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
516
|
+
const planAction = await p['select']({
|
|
517
|
+
message: 'Plan generated. What would you like to do?',
|
|
518
|
+
options: [
|
|
519
|
+
{ value: 'proceed', label: 'Proceed with plan', hint: 'Executes the generated plan immediately' },
|
|
520
|
+
{ value: 'edit', label: 'Edit plan', hint: 'Provide additional feedback to revise the plan' },
|
|
521
|
+
{ value: 'exit', label: 'Exit plan mode', hint: 'Return to normal chat without executing' },
|
|
522
|
+
],
|
|
523
|
+
});
|
|
524
|
+
if (p.isCancel(planAction) || planAction === 'exit') {
|
|
525
|
+
planModeReturn = 'exit';
|
|
526
|
+
}
|
|
527
|
+
else if (planAction === 'edit') {
|
|
528
|
+
planModeReturn = 'edit';
|
|
529
|
+
}
|
|
530
|
+
else if (planAction === 'proceed') {
|
|
531
|
+
planModeReturn = 'proceed';
|
|
532
|
+
}
|
|
322
533
|
}
|
|
534
|
+
return { planModeReturn, contextResult: gatherRes.contextResult };
|
|
323
535
|
}
|
|
324
536
|
catch (err) {
|
|
325
|
-
spinner.stop('');
|
|
326
537
|
const message = err instanceof Error ? err.message : String(err);
|
|
327
538
|
p.log.error(`${pc.red('Error:')} ${message}`);
|
|
328
539
|
}
|
|
329
540
|
finally {
|
|
541
|
+
spinner.stop();
|
|
542
|
+
process.stdout.write('\x1b[2K\r');
|
|
330
543
|
// Persist any verified or partial changes into our session change ledger.
|
|
331
544
|
// This ensures that if the process crashes, errors out, or is aborted mid-generation,
|
|
332
545
|
// the user can still use /revert to undo the partial file mutations.
|
|
546
|
+
const changedFiles = changeLogger.getChangedFiles();
|
|
333
547
|
changeLogger.commitChangeSet();
|
|
548
|
+
// Delta-update the embedding index if there were changes
|
|
549
|
+
if (changedFiles.length > 0) {
|
|
550
|
+
try {
|
|
551
|
+
const { getEmbeddingIndex } = await import('./embeddingIndex.js');
|
|
552
|
+
const index = getEmbeddingIndex();
|
|
553
|
+
// Only update if it's already loaded in memory
|
|
554
|
+
if (index.isReady()) {
|
|
555
|
+
await index.updateIndex(workspaceRoot, changedFiles);
|
|
556
|
+
await index.save(workspaceRoot);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
catch (e) {
|
|
560
|
+
debugLog(`Failed to delta-update embedding index: ${e.message}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
334
563
|
}
|
|
335
|
-
}
|
|
564
|
+
});
|
|
336
565
|
}
|
|
337
|
-
async function collectUserInput() {
|
|
566
|
+
async function collectUserInput(history, isPlanMode) {
|
|
338
567
|
let lines = [];
|
|
339
568
|
let isMultiLine = false;
|
|
340
569
|
while (true) {
|
|
341
|
-
const userInputRaw = await
|
|
342
|
-
message: isMultiLine ? ' ' : pc.magenta('❯'),
|
|
570
|
+
const userInputRaw = await historyText({
|
|
571
|
+
message: isMultiLine ? ' ' : isPlanMode ? pc.cyan('(Plan Mode) ❯') : pc.magenta('❯'),
|
|
343
572
|
placeholder: isMultiLine ? '' : 'Use "\\" for new lines',
|
|
573
|
+
history,
|
|
344
574
|
});
|
|
345
575
|
if (p.isCancel(userInputRaw)) {
|
|
346
576
|
if (isMultiLine) {
|
|
@@ -376,10 +606,12 @@ async function compressContextFiles(workspaceRoot, contextResult) {
|
|
|
376
606
|
const cachedContext = readCache(workspaceRoot, 'context_cache.json') || {};
|
|
377
607
|
let cacheUpdated = false;
|
|
378
608
|
const compressedFiles = new Map();
|
|
379
|
-
for (const [filePath,
|
|
380
|
-
|
|
609
|
+
for (const [filePath, contentObj] of contextResult.relevantFiles.entries()) {
|
|
610
|
+
const content = contentObj.text;
|
|
611
|
+
const inlineData = contentObj.inlineData;
|
|
612
|
+
if (content.length < 2000 && !inlineData) {
|
|
381
613
|
debugLog(`Bypassing cache and compression for ${filePath} (length ${content.length} < 2000)`);
|
|
382
|
-
compressedFiles.set(filePath,
|
|
614
|
+
compressedFiles.set(filePath, contentObj);
|
|
383
615
|
continue;
|
|
384
616
|
}
|
|
385
617
|
const fileHash = crypto
|
|
@@ -388,13 +620,13 @@ async function compressContextFiles(workspaceRoot, contextResult) {
|
|
|
388
620
|
.digest('hex');
|
|
389
621
|
if (cachedContext[fileHash]) {
|
|
390
622
|
debugLog(`Context cache HIT for file ${filePath} (hash ${fileHash.substring(0, 8)})`);
|
|
391
|
-
compressedFiles.set(filePath, cachedContext[fileHash]);
|
|
623
|
+
compressedFiles.set(filePath, { text: cachedContext[fileHash] });
|
|
392
624
|
}
|
|
393
625
|
else {
|
|
394
626
|
debugLog(`Context cache MISS for file ${filePath} (hash ${fileHash.substring(0, 8)})`);
|
|
395
627
|
const compressPrompt = `Summarize the following file contents concisely. Preserve all exports, functions, classes, variables, and architectural purpose. Keep it under 2000 characters if possible. File: ${filePath}`;
|
|
396
|
-
const summary = await compressTextUsingFlashLite(content, compressPrompt);
|
|
397
|
-
compressedFiles.set(filePath, summary);
|
|
628
|
+
const summary = await compressTextUsingFlashLite(content, compressPrompt, inlineData);
|
|
629
|
+
compressedFiles.set(filePath, { text: summary });
|
|
398
630
|
cachedContext[fileHash] = summary;
|
|
399
631
|
cacheUpdated = true;
|
|
400
632
|
}
|
|
@@ -441,8 +673,12 @@ async function executeSelfCorrectionLoop(chat, initialResult, workspaceRoot, inp
|
|
|
441
673
|
const forcePrompt = `AUTOMATED SYSTEM CHECK: You did not modify any files. You MUST use your file modification tools to apply a fix for the previously mentioned errors. Do not just explain the issue.`;
|
|
442
674
|
spinner.start('Thinking (Correction)...');
|
|
443
675
|
result = await chat.sendMessage(forcePrompt);
|
|
444
|
-
spinner.stop(
|
|
676
|
+
spinner.stop();
|
|
677
|
+
process.stdout.write('\x1b[2K\r');
|
|
445
678
|
correctionAttempts++;
|
|
679
|
+
const collector = getMetricCollector();
|
|
680
|
+
if (collector)
|
|
681
|
+
collector.recordSelfCorrection();
|
|
446
682
|
continue;
|
|
447
683
|
}
|
|
448
684
|
previousChangeCount = currentChanges.length;
|
|
@@ -466,9 +702,17 @@ async function executeSelfCorrectionLoop(chat, initialResult, workspaceRoot, inp
|
|
|
466
702
|
}
|
|
467
703
|
if (!hasErrors) {
|
|
468
704
|
p.log.success('Verification passed.');
|
|
705
|
+
const collector = getMetricCollector();
|
|
706
|
+
if (collector && correctionAttempts === 0)
|
|
707
|
+
collector.recordVerificationResult(true);
|
|
469
708
|
break;
|
|
470
709
|
}
|
|
710
|
+
const collector = getMetricCollector();
|
|
711
|
+
if (collector && correctionAttempts === 0)
|
|
712
|
+
collector.recordVerificationResult(false);
|
|
471
713
|
correctionAttempts++;
|
|
714
|
+
if (collector)
|
|
715
|
+
collector.recordSelfCorrection();
|
|
472
716
|
if (correctionAttempts > MAX_CORRECTIONS) {
|
|
473
717
|
p.log.warn('Max self-correction attempts reached. Leaving remaining issues for manual review.');
|
|
474
718
|
break;
|
|
@@ -496,7 +740,8 @@ async function executeSelfCorrectionLoop(chat, initialResult, workspaceRoot, inp
|
|
|
496
740
|
const correctionPrompt = `AUTOMATED SYSTEM CHECK: Your previous changes resulted in the following issues:\n\n${combinedIssuesForAI}\n\nPlease analyze these issues and use your file modification tools to fix them.`;
|
|
497
741
|
spinner.start('Thinking (Correction)...');
|
|
498
742
|
result = await chat.sendMessage(correctionPrompt);
|
|
499
|
-
spinner.stop(
|
|
743
|
+
spinner.stop();
|
|
744
|
+
process.stdout.write('\x1b[2K\r');
|
|
500
745
|
}
|
|
501
746
|
return { finalText };
|
|
502
747
|
}
|
package/dist/services/ai.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { Content, FunctionCall } from '@google/generative-ai';
|
|
2
|
+
export declare function setModelOverride(agent: 'context' | 'execution', overrideStr: string): void;
|
|
3
|
+
export declare function clearModelOverrides(): void;
|
|
4
|
+
export declare function setGlobalActiveModel(model: string): void;
|
|
5
|
+
export declare function getGlobalActiveModel(): string;
|
|
6
|
+
export declare function getGlobalLatestUsageMetadata(): any;
|
|
2
7
|
export declare class ProxyChatSession {
|
|
3
8
|
private history;
|
|
4
9
|
private modelName;
|
|
@@ -31,7 +36,7 @@ export declare class ProxyChatSession {
|
|
|
31
36
|
name: string;
|
|
32
37
|
response: any;
|
|
33
38
|
};
|
|
34
|
-
}>, additionalText?: string, abortSignal?: AbortSignal): Promise<{
|
|
39
|
+
}>, additionalText?: string, abortSignal?: AbortSignal, onChunk?: (chunk: string) => void): Promise<{
|
|
35
40
|
response: {
|
|
36
41
|
text: () => string;
|
|
37
42
|
functionCalls: () => FunctionCall[] | undefined;
|
|
@@ -56,16 +61,25 @@ export declare function getPlanExecutionConfig(): {
|
|
|
56
61
|
functionDeclarations: import("@google/generative-ai").FunctionDeclaration[];
|
|
57
62
|
}[];
|
|
58
63
|
};
|
|
64
|
+
export declare function getPlanModeConfig(): {
|
|
65
|
+
systemInstruction: string;
|
|
66
|
+
tools: never[];
|
|
67
|
+
};
|
|
59
68
|
/**
|
|
60
69
|
* Compresses a large string of text using gemini-3.1-flash-lite.
|
|
61
70
|
* Used for shrinking context payloads to prevent OOM/choking.
|
|
62
71
|
*/
|
|
63
|
-
export declare function compressTextUsingFlashLite(text: string, instruction?: string): Promise<string>;
|
|
64
|
-
|
|
72
|
+
export declare function compressTextUsingFlashLite(text: string, instruction?: string, inlineData?: any): Promise<string>;
|
|
73
|
+
/**
|
|
74
|
+
* Returns the tool declarations for the read-only Context Agent.
|
|
75
|
+
* Extracted as a reusable function so investigation sub-agents can import
|
|
76
|
+
* the same schema without duplicating the declarations.
|
|
77
|
+
*/
|
|
78
|
+
export declare function getContextToolDeclarations(): any[];
|
|
65
79
|
export declare function createContextAgentSession(): any;
|
|
66
|
-
export declare const INTENT_ROUTER_MODEL: "gemini-3.1-flash-lite";
|
|
67
80
|
export declare function createIntentRouterSession(): any;
|
|
68
|
-
export declare
|
|
81
|
+
export declare function createExecutionComplexitySession(): any;
|
|
82
|
+
export declare function createInvestigationComplexitySession(): any;
|
|
69
83
|
export declare function createWebSearchAgentSession(): any;
|
|
70
84
|
/**
|
|
71
85
|
* Generates a concise title for a chat session based on the user's first message.
|