minovative-mind-cli 1.5.0 → 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 +10 -5
- package/dist/services/agent/slashCommands.js +163 -37
- 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 +294 -38
- 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.d.ts +2 -0
- package/dist/utils/logo.js +31 -10
- 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 +5 -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,21 +275,147 @@ 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);
|
|
229
|
-
if (
|
|
230
|
-
|
|
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
|
+
}
|
|
321
|
+
if (gatherRes.contextResult) {
|
|
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
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
// Parallel investigation already printed its own detailed summary log
|
|
333
|
+
if (!inputHandler.isCurrentlyPrompting()) {
|
|
334
|
+
spinner.start('Thinking...');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
if (!inputHandler.isCurrentlyPrompting()) {
|
|
340
|
+
spinner.message('Thinking...');
|
|
341
|
+
}
|
|
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
|
+
}
|
|
231
419
|
}
|
|
232
420
|
// Send the formulated prompt payload to the generative model
|
|
233
421
|
let result;
|
|
@@ -235,21 +423,23 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
235
423
|
result = await chat.sendMessage(finalInput, undefined, ac.signal);
|
|
236
424
|
}
|
|
237
425
|
catch (e) {
|
|
238
|
-
spinner.stop(
|
|
426
|
+
spinner.stop();
|
|
427
|
+
process.stdout.write('\x1b[2K\r');
|
|
239
428
|
if (e.name === 'AbortError' || e.message?.includes('abort')) {
|
|
240
429
|
p.log.warn(pc.yellow('Generation aborted by user.'));
|
|
241
430
|
}
|
|
242
431
|
else {
|
|
243
432
|
p.log.error(pc.red(`Error communicating with AI: ${e.message || String(e)}`));
|
|
244
433
|
}
|
|
245
|
-
|
|
434
|
+
return;
|
|
246
435
|
}
|
|
247
436
|
latestUsage = result.response.usageMetadata?.();
|
|
248
437
|
const grounding = result.response.groundingMetadata?.();
|
|
249
438
|
if (grounding?.webSearchQueries && grounding.webSearchQueries.length > 0) {
|
|
250
439
|
p.log.info(`${pc.blue('🌐')} ${pc.dim('Google Search Queries:')} ${grounding.webSearchQueries.map((q) => pc.cyan(`"${q}"`)).join(', ')}`);
|
|
251
440
|
}
|
|
252
|
-
spinner.stop(
|
|
441
|
+
spinner.stop();
|
|
442
|
+
process.stdout.write('\x1b[2K\r');
|
|
253
443
|
const calls = result.response.functionCalls();
|
|
254
444
|
debugLog(`Initial functionCalls: ${calls && calls.length > 0 ? JSON.stringify(calls) : 'None'}`);
|
|
255
445
|
// Stage 2: Recursive Tool Loops and Automated Self-Correction (delegated to a helper function to avoid nested loop warning)
|
|
@@ -292,44 +482,95 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
292
482
|
if (history.length > 0) {
|
|
293
483
|
if (!chatSessionState.title) {
|
|
294
484
|
chatSessionState.title = 'Generating title...';
|
|
295
|
-
|
|
485
|
+
try {
|
|
486
|
+
const title = await generateChatTitle(userInput);
|
|
296
487
|
chatSessionState.title = title;
|
|
297
|
-
chatHistoryService.saveSession({
|
|
488
|
+
await chatHistoryService.saveSession({
|
|
298
489
|
id: chatSessionState.id,
|
|
299
490
|
title: chatSessionState.title,
|
|
300
491
|
timestamp: Date.now(),
|
|
301
|
-
history: chat.getRawHistory()
|
|
302
|
-
})
|
|
303
|
-
}
|
|
492
|
+
history: chat.getRawHistory(),
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
catch (e) {
|
|
496
|
+
debugLog('Failed to save session with title: ' + e);
|
|
497
|
+
}
|
|
304
498
|
}
|
|
305
|
-
chatHistoryService
|
|
499
|
+
chatHistoryService
|
|
500
|
+
.saveSession({
|
|
306
501
|
id: chatSessionState.id,
|
|
307
502
|
title: chatSessionState.title,
|
|
308
503
|
timestamp: Date.now(),
|
|
309
|
-
history
|
|
310
|
-
})
|
|
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
|
+
}
|
|
311
533
|
}
|
|
534
|
+
return { planModeReturn, contextResult: gatherRes.contextResult };
|
|
312
535
|
}
|
|
313
536
|
catch (err) {
|
|
314
|
-
spinner.stop('');
|
|
315
537
|
const message = err instanceof Error ? err.message : String(err);
|
|
316
538
|
p.log.error(`${pc.red('Error:')} ${message}`);
|
|
317
539
|
}
|
|
318
540
|
finally {
|
|
541
|
+
spinner.stop();
|
|
542
|
+
process.stdout.write('\x1b[2K\r');
|
|
319
543
|
// Persist any verified or partial changes into our session change ledger.
|
|
320
544
|
// This ensures that if the process crashes, errors out, or is aborted mid-generation,
|
|
321
545
|
// the user can still use /revert to undo the partial file mutations.
|
|
546
|
+
const changedFiles = changeLogger.getChangedFiles();
|
|
322
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
|
+
}
|
|
323
563
|
}
|
|
324
|
-
}
|
|
564
|
+
});
|
|
325
565
|
}
|
|
326
|
-
async function collectUserInput() {
|
|
566
|
+
async function collectUserInput(history, isPlanMode) {
|
|
327
567
|
let lines = [];
|
|
328
568
|
let isMultiLine = false;
|
|
329
569
|
while (true) {
|
|
330
|
-
const userInputRaw = await
|
|
331
|
-
message: isMultiLine ? ' ' : pc.magenta('❯'),
|
|
570
|
+
const userInputRaw = await historyText({
|
|
571
|
+
message: isMultiLine ? ' ' : isPlanMode ? pc.cyan('(Plan Mode) ❯') : pc.magenta('❯'),
|
|
332
572
|
placeholder: isMultiLine ? '' : 'Use "\\" for new lines',
|
|
573
|
+
history,
|
|
333
574
|
});
|
|
334
575
|
if (p.isCancel(userInputRaw)) {
|
|
335
576
|
if (isMultiLine) {
|
|
@@ -365,10 +606,12 @@ async function compressContextFiles(workspaceRoot, contextResult) {
|
|
|
365
606
|
const cachedContext = readCache(workspaceRoot, 'context_cache.json') || {};
|
|
366
607
|
let cacheUpdated = false;
|
|
367
608
|
const compressedFiles = new Map();
|
|
368
|
-
for (const [filePath,
|
|
369
|
-
|
|
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) {
|
|
370
613
|
debugLog(`Bypassing cache and compression for ${filePath} (length ${content.length} < 2000)`);
|
|
371
|
-
compressedFiles.set(filePath,
|
|
614
|
+
compressedFiles.set(filePath, contentObj);
|
|
372
615
|
continue;
|
|
373
616
|
}
|
|
374
617
|
const fileHash = crypto
|
|
@@ -377,13 +620,13 @@ async function compressContextFiles(workspaceRoot, contextResult) {
|
|
|
377
620
|
.digest('hex');
|
|
378
621
|
if (cachedContext[fileHash]) {
|
|
379
622
|
debugLog(`Context cache HIT for file ${filePath} (hash ${fileHash.substring(0, 8)})`);
|
|
380
|
-
compressedFiles.set(filePath, cachedContext[fileHash]);
|
|
623
|
+
compressedFiles.set(filePath, { text: cachedContext[fileHash] });
|
|
381
624
|
}
|
|
382
625
|
else {
|
|
383
626
|
debugLog(`Context cache MISS for file ${filePath} (hash ${fileHash.substring(0, 8)})`);
|
|
384
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}`;
|
|
385
|
-
const summary = await compressTextUsingFlashLite(content, compressPrompt);
|
|
386
|
-
compressedFiles.set(filePath, summary);
|
|
628
|
+
const summary = await compressTextUsingFlashLite(content, compressPrompt, inlineData);
|
|
629
|
+
compressedFiles.set(filePath, { text: summary });
|
|
387
630
|
cachedContext[fileHash] = summary;
|
|
388
631
|
cacheUpdated = true;
|
|
389
632
|
}
|
|
@@ -430,8 +673,12 @@ async function executeSelfCorrectionLoop(chat, initialResult, workspaceRoot, inp
|
|
|
430
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.`;
|
|
431
674
|
spinner.start('Thinking (Correction)...');
|
|
432
675
|
result = await chat.sendMessage(forcePrompt);
|
|
433
|
-
spinner.stop(
|
|
676
|
+
spinner.stop();
|
|
677
|
+
process.stdout.write('\x1b[2K\r');
|
|
434
678
|
correctionAttempts++;
|
|
679
|
+
const collector = getMetricCollector();
|
|
680
|
+
if (collector)
|
|
681
|
+
collector.recordSelfCorrection();
|
|
435
682
|
continue;
|
|
436
683
|
}
|
|
437
684
|
previousChangeCount = currentChanges.length;
|
|
@@ -455,9 +702,17 @@ async function executeSelfCorrectionLoop(chat, initialResult, workspaceRoot, inp
|
|
|
455
702
|
}
|
|
456
703
|
if (!hasErrors) {
|
|
457
704
|
p.log.success('Verification passed.');
|
|
705
|
+
const collector = getMetricCollector();
|
|
706
|
+
if (collector && correctionAttempts === 0)
|
|
707
|
+
collector.recordVerificationResult(true);
|
|
458
708
|
break;
|
|
459
709
|
}
|
|
710
|
+
const collector = getMetricCollector();
|
|
711
|
+
if (collector && correctionAttempts === 0)
|
|
712
|
+
collector.recordVerificationResult(false);
|
|
460
713
|
correctionAttempts++;
|
|
714
|
+
if (collector)
|
|
715
|
+
collector.recordSelfCorrection();
|
|
461
716
|
if (correctionAttempts > MAX_CORRECTIONS) {
|
|
462
717
|
p.log.warn('Max self-correction attempts reached. Leaving remaining issues for manual review.');
|
|
463
718
|
break;
|
|
@@ -485,7 +740,8 @@ async function executeSelfCorrectionLoop(chat, initialResult, workspaceRoot, inp
|
|
|
485
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.`;
|
|
486
741
|
spinner.start('Thinking (Correction)...');
|
|
487
742
|
result = await chat.sendMessage(correctionPrompt);
|
|
488
|
-
spinner.stop(
|
|
743
|
+
spinner.stop();
|
|
744
|
+
process.stdout.write('\x1b[2K\r');
|
|
489
745
|
}
|
|
490
746
|
return { finalText };
|
|
491
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.
|