minovative-mind-cli 1.5.1 → 2.1.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 +59 -45
- package/dist/commands/chat.js +10 -2
- package/dist/services/agent/slashCommands.js +369 -42
- 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 +272 -34
- package/dist/services/agent.d.ts +8 -0
- package/dist/services/agent.js +288 -40
- package/dist/services/ai.d.ts +19 -5
- package/dist/services/ai.js +182 -36
- 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 +112 -19
- 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 +362 -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 +217 -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 +190 -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/services/workspaceRegistry.d.ts +137 -0
- package/dist/services/workspaceRegistry.js +270 -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 +10 -3
- 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/pathSecurity.d.ts +31 -0
- package/dist/utils/pathSecurity.js +48 -0
- 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 +111 -6
- 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,29 @@ 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: '/workspaces', label: '/workspaces', hint: 'Manage external workspaces for cross-project development' },
|
|
144
|
+
{ value: '/stats', label: '/stats', hint: 'View current session statistics and configuration' },
|
|
124
145
|
{ value: '/commit', label: '/commit', hint: 'Auto-commit changes with AI message' },
|
|
125
146
|
{ value: '/revert', label: '/revert', hint: 'Undo last change' },
|
|
126
147
|
{ value: '/chats', label: '/chats', hint: 'View or resume past chat sessions' },
|
|
@@ -151,12 +172,16 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
151
172
|
workspaceRoot,
|
|
152
173
|
version,
|
|
153
174
|
isRawPasteMode,
|
|
175
|
+
isPlanMode,
|
|
154
176
|
chatSessionState,
|
|
155
177
|
};
|
|
156
178
|
const slashResult = await handleSlashCommand(userInput, slashCtx);
|
|
157
179
|
if (slashResult.isRawPasteMode !== undefined) {
|
|
158
180
|
isRawPasteMode = slashResult.isRawPasteMode;
|
|
159
181
|
}
|
|
182
|
+
if (slashResult.isPlanModeOverride !== undefined) {
|
|
183
|
+
isPlanMode = slashResult.isPlanModeOverride;
|
|
184
|
+
}
|
|
160
185
|
if (slashResult.userInputOverride !== undefined) {
|
|
161
186
|
userInput = slashResult.userInputOverride;
|
|
162
187
|
}
|
|
@@ -165,6 +190,36 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
165
190
|
}
|
|
166
191
|
}
|
|
167
192
|
// ─── Main Execution Cycle ────────────────────────────────────────
|
|
193
|
+
let currentInput = userInput;
|
|
194
|
+
let cachedContextResult = null;
|
|
195
|
+
while (true) {
|
|
196
|
+
const execResult = await executeSingleTurn(workspaceRoot, currentInput, chat, inputHandler, chatSessionState, isPlanMode, cachedContextResult);
|
|
197
|
+
const planModeReturn = execResult?.planModeReturn;
|
|
198
|
+
const contextResult = execResult?.contextResult;
|
|
199
|
+
if (isPlanMode && planModeReturn) {
|
|
200
|
+
if (planModeReturn === 'proceed') {
|
|
201
|
+
isPlanMode = false;
|
|
202
|
+
currentInput = 'Please execute the plan we just finalized.';
|
|
203
|
+
cachedContextResult = contextResult;
|
|
204
|
+
p.log.info(pc.cyan('Proceeding with execution...'));
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
else if (planModeReturn === 'edit') {
|
|
208
|
+
p.log.info(pc.cyan('Please provide additional instructions to revise the plan.'));
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
else if (planModeReturn === 'exit') {
|
|
212
|
+
isPlanMode = false;
|
|
213
|
+
p.log.info(pc.cyan('Exited plan mode. Returning to normal chat.'));
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHandler, chatSessionState, isPlanMode, cachedContextResult) {
|
|
222
|
+
return runWithAgentId('main', async () => {
|
|
168
223
|
const turnStartTime = Date.now();
|
|
169
224
|
const spinner = p.spinner();
|
|
170
225
|
const ac = new AbortController();
|
|
@@ -175,12 +230,20 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
175
230
|
let finalInput = userInput;
|
|
176
231
|
// Stage 1: Gather Workspace Context and route intentions
|
|
177
232
|
spinner.start('🔍 Investigating workspace...');
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
233
|
+
let gatherRes = { targetAgent: 'EXECUTE', chainedMessages: [], contextResult: cachedContextResult || null };
|
|
234
|
+
if (!cachedContextResult) {
|
|
235
|
+
const collector = getMetricCollector();
|
|
236
|
+
if (collector)
|
|
237
|
+
collector.startTimer('contextGather');
|
|
238
|
+
const chatHistory = chat.getRecentHistory(3);
|
|
239
|
+
gatherRes = await gatherContext(workspaceRoot, userInput, chatHistory, inputHandler, ac.signal, (msg) => {
|
|
240
|
+
if (!inputHandler.isCurrentlyPrompting()) {
|
|
241
|
+
spinner.message(`🔍 Investigating workspace... ${pc.dim(msg)}`);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
if (collector)
|
|
245
|
+
collector.stopTimer('contextGather');
|
|
246
|
+
}
|
|
184
247
|
let latestUsage = undefined;
|
|
185
248
|
// Collect any inputs that were queued while the Context Agent was investigating
|
|
186
249
|
const leftoverMsg = inputHandler.getAndClear();
|
|
@@ -213,26 +276,66 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
213
276
|
}
|
|
214
277
|
// Hot-swap the underlying LLM system instruction context depending on intent (conversational vs execution)
|
|
215
278
|
debugLog(`Intent Router output: original targetAgent = ${gatherRes.targetAgent}, effective = ${effectiveTargetAgent}`);
|
|
216
|
-
|
|
279
|
+
let config;
|
|
280
|
+
if (isPlanMode) {
|
|
281
|
+
const { getPlanModeConfig } = await import('./ai.js');
|
|
282
|
+
config = getPlanModeConfig();
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
config = effectiveTargetAgent === 'CHAT' ? getGeneralChatConfig() : getPlanExecutionConfig();
|
|
286
|
+
}
|
|
217
287
|
let dynamicSystemInstruction = config.systemInstruction;
|
|
218
288
|
// Inject gathered workspace directories, dependency configurations, and matching search patterns
|
|
219
289
|
if (gatherRes.contextResult) {
|
|
220
290
|
// Compress each relevant file individually using helper to avoid nested loop warning
|
|
221
|
-
|
|
291
|
+
if (!cachedContextResult) {
|
|
292
|
+
gatherRes.contextResult.relevantFiles = await compressContextFiles(workspaceRoot, gatherRes.contextResult);
|
|
293
|
+
}
|
|
222
294
|
// Assemble the final context injection string
|
|
223
295
|
const contextInjection = buildContextInjection(gatherRes.contextResult);
|
|
224
296
|
debugLog(`Final compressed context injection size: ${contextInjection.length} chars`);
|
|
297
|
+
const collector = getMetricCollector();
|
|
298
|
+
if (collector)
|
|
299
|
+
collector.recordCompressedContextSize(contextInjection.length);
|
|
225
300
|
dynamicSystemInstruction += '\n\n' + contextInjection;
|
|
226
301
|
}
|
|
227
302
|
// Apply the dynamic prompt updates and tool registrations to the active chat session
|
|
228
303
|
chat.setAgentConfig(dynamicSystemInstruction, config.tools);
|
|
304
|
+
if (getGlobalActiveModel() === GEMINI_MODELS.AUTO) {
|
|
305
|
+
let selectedModel = GEMINI_MODELS.FLASH_3_5;
|
|
306
|
+
if (effectiveTargetAgent === 'CHAT') {
|
|
307
|
+
selectedModel = GEMINI_MODELS.FLASH_LITE_3_1;
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
spinner.stop(); // MUST clear the investigation spinner first to prevent leaking the setInterval
|
|
311
|
+
spinner.start(pc.blue('🧠 Evaluating execution complexity...'));
|
|
312
|
+
const complexity = await evaluateExecutionComplexity(finalInput, gatherRes.contextResult?.summary, gatherRes.contextResult?.relevantFiles?.size || 0);
|
|
313
|
+
spinner.stop();
|
|
314
|
+
process.stdout.write('\x1b[2K\r');
|
|
315
|
+
selectedModel = complexity === 'EASY' ? GEMINI_MODELS.FLASH_LITE_3_1 : GEMINI_MODELS.FLASH_3_5;
|
|
316
|
+
}
|
|
317
|
+
chat.setModel(selectedModel);
|
|
318
|
+
debugLog(`Auto-routing to model ${selectedModel} based on intent ${effectiveTargetAgent}`);
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
chat.setModel(getGlobalActiveModel());
|
|
322
|
+
}
|
|
229
323
|
if (gatherRes.contextResult) {
|
|
230
|
-
if (!
|
|
231
|
-
|
|
232
|
-
|
|
324
|
+
if (!gatherRes.contextResult.isParallel) {
|
|
325
|
+
if (!inputHandler.isCurrentlyPrompting()) {
|
|
326
|
+
spinner.stop(pc.green('🔍 Investigation completed.'));
|
|
327
|
+
spinner.start('Thinking...');
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
p.log.success(pc.green('🔍 Investigation completed.'));
|
|
331
|
+
}
|
|
233
332
|
}
|
|
234
333
|
else {
|
|
235
|
-
|
|
334
|
+
// Parallel investigation already printed its own detailed summary log
|
|
335
|
+
if (!inputHandler.isCurrentlyPrompting()) {
|
|
336
|
+
spinner.stop(); // MUST clear the investigation spinner first to prevent leaking the setInterval
|
|
337
|
+
spinner.start('Thinking...');
|
|
338
|
+
}
|
|
236
339
|
}
|
|
237
340
|
}
|
|
238
341
|
else {
|
|
@@ -240,27 +343,106 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
240
343
|
spinner.message('Thinking...');
|
|
241
344
|
}
|
|
242
345
|
}
|
|
346
|
+
if (effectiveTargetAgent === 'EXECUTE' && isSubAgentsEnabled()) {
|
|
347
|
+
spinner.stop(); // Clear the spinner before delegating
|
|
348
|
+
process.stdout.write('\x1b[2K\r');
|
|
349
|
+
const orchestrator = new Orchestrator(workspaceRoot, chatSessionState.id, inputHandler);
|
|
350
|
+
const handledByOrchestrator = await orchestrator.runOrchestration(finalInput, dynamicSystemInstruction, ac.signal);
|
|
351
|
+
if (typeof handledByOrchestrator === 'string') {
|
|
352
|
+
// The orchestrator successfully decomposed and ran the task.
|
|
353
|
+
// Inject the task context into the main proxy chat session so the agent remembers what happened
|
|
354
|
+
// and so the history state gets saved properly on disk.
|
|
355
|
+
chat.getRawHistory().push({
|
|
356
|
+
role: 'user',
|
|
357
|
+
parts: [{ text: userInput }],
|
|
358
|
+
});
|
|
359
|
+
chat.getRawHistory().push({
|
|
360
|
+
role: 'model',
|
|
361
|
+
parts: [{ text: handledByOrchestrator }],
|
|
362
|
+
});
|
|
363
|
+
// Print the summary text just like single-agent mode
|
|
364
|
+
if (handledByOrchestrator !== '[Generation stopped.]') {
|
|
365
|
+
console.log(`\n${pc.blue('◆')} ${pc.bold('Minovative Mind')} ${pc.dim(`(Orchestrator)`)}\n`);
|
|
366
|
+
const cleanText = handledByOrchestrator.replace(/\n([ \t]*\n){2,}/g, '\n\n');
|
|
367
|
+
console.log(marked.parse(cleanText));
|
|
368
|
+
}
|
|
369
|
+
// Print Usage Stats
|
|
370
|
+
const usage = getGlobalLatestUsageMetadata();
|
|
371
|
+
if (usage) {
|
|
372
|
+
if (usage.cachedTokens > 0) {
|
|
373
|
+
const totalInputTokens = (usage.promptTokens || 0) + usage.cachedTokens;
|
|
374
|
+
const percentSaved = totalInputTokens > 0 ? Math.round((usage.cachedTokens / totalInputTokens) * 100) : 0;
|
|
375
|
+
p.log.info(`${pc.green('⚡')} ${pc.green('Context Cache Hit:')} ${pc.bold(usage.cachedTokens.toLocaleString())} tokens cached ${pc.dim(`(Saved ~${percentSaved}% of input cost)`)}`);
|
|
376
|
+
}
|
|
377
|
+
if (usage.remainingBalance !== undefined) {
|
|
378
|
+
p.log.info(`${pc.dim('Credits Remaining:')} ${pc.cyan(usage.remainingBalance.toLocaleString())}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Post-execution steps for the UI (saving history, invalidate cache)
|
|
382
|
+
const postExecutionChanges = changeLogger.getCurrentChangeSet()?.changes || [];
|
|
383
|
+
const modifiedFiles = postExecutionChanges
|
|
384
|
+
.filter((c) => c.action === 'modify' || c.action === 'create' || c.action === 'delete')
|
|
385
|
+
.map((c) => path.relative(workspaceRoot, c.filePath));
|
|
386
|
+
if (modifiedFiles.length > 0) {
|
|
387
|
+
await invalidateCacheForDependents(workspaceRoot, modifiedFiles);
|
|
388
|
+
}
|
|
389
|
+
const turnEndTime = Date.now();
|
|
390
|
+
const turnDuration = ((turnEndTime - turnStartTime) / 1000).toFixed(1);
|
|
391
|
+
p.log.info(`${pc.dim('Orchestration finished in')} ${pc.cyan(turnDuration + 's')}`);
|
|
392
|
+
changeLogger.markComplete();
|
|
393
|
+
changeLogger.commitChangeSet();
|
|
394
|
+
// Auto-save chat history for the orchestrator turn
|
|
395
|
+
const history = chat.getRawHistory();
|
|
396
|
+
if (history.length > 0) {
|
|
397
|
+
if (!chatSessionState.title) {
|
|
398
|
+
chatSessionState.title = 'Generating title...';
|
|
399
|
+
try {
|
|
400
|
+
const title = await generateChatTitle(userInput);
|
|
401
|
+
chatSessionState.title = title;
|
|
402
|
+
}
|
|
403
|
+
catch (e) {
|
|
404
|
+
debugLog('Failed to save session with title: ' + e);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
chatHistoryService
|
|
408
|
+
.saveSession({
|
|
409
|
+
id: chatSessionState.id,
|
|
410
|
+
title: chatSessionState.title,
|
|
411
|
+
timestamp: Date.now(),
|
|
412
|
+
history,
|
|
413
|
+
})
|
|
414
|
+
.catch((e) => debugLog('Failed to auto-save session: ' + e));
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
// If it returned false, the graph was too simple (1 task) or failed, so we fall through to the single-agent loop.
|
|
419
|
+
if (!inputHandler.isCurrentlyPrompting()) {
|
|
420
|
+
spinner.start('Thinking (Single-Agent)...');
|
|
421
|
+
}
|
|
422
|
+
}
|
|
243
423
|
// Send the formulated prompt payload to the generative model
|
|
244
424
|
let result;
|
|
245
425
|
try {
|
|
246
426
|
result = await chat.sendMessage(finalInput, undefined, ac.signal);
|
|
247
427
|
}
|
|
248
428
|
catch (e) {
|
|
249
|
-
spinner.stop(
|
|
429
|
+
spinner.stop();
|
|
430
|
+
process.stdout.write('\x1b[2K\r');
|
|
250
431
|
if (e.name === 'AbortError' || e.message?.includes('abort')) {
|
|
251
432
|
p.log.warn(pc.yellow('Generation aborted by user.'));
|
|
252
433
|
}
|
|
253
434
|
else {
|
|
254
435
|
p.log.error(pc.red(`Error communicating with AI: ${e.message || String(e)}`));
|
|
255
436
|
}
|
|
256
|
-
|
|
437
|
+
return;
|
|
257
438
|
}
|
|
258
439
|
latestUsage = result.response.usageMetadata?.();
|
|
259
440
|
const grounding = result.response.groundingMetadata?.();
|
|
260
441
|
if (grounding?.webSearchQueries && grounding.webSearchQueries.length > 0) {
|
|
261
442
|
p.log.info(`${pc.blue('🌐')} ${pc.dim('Google Search Queries:')} ${grounding.webSearchQueries.map((q) => pc.cyan(`"${q}"`)).join(', ')}`);
|
|
262
443
|
}
|
|
263
|
-
spinner.stop(
|
|
444
|
+
spinner.stop();
|
|
445
|
+
process.stdout.write('\x1b[2K\r');
|
|
264
446
|
const calls = result.response.functionCalls();
|
|
265
447
|
debugLog(`Initial functionCalls: ${calls && calls.length > 0 ? JSON.stringify(calls) : 'None'}`);
|
|
266
448
|
// Stage 2: Recursive Tool Loops and Automated Self-Correction (delegated to a helper function to avoid nested loop warning)
|
|
@@ -303,44 +485,95 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
303
485
|
if (history.length > 0) {
|
|
304
486
|
if (!chatSessionState.title) {
|
|
305
487
|
chatSessionState.title = 'Generating title...';
|
|
306
|
-
|
|
488
|
+
try {
|
|
489
|
+
const title = await generateChatTitle(userInput);
|
|
307
490
|
chatSessionState.title = title;
|
|
308
|
-
chatHistoryService.saveSession({
|
|
491
|
+
await chatHistoryService.saveSession({
|
|
309
492
|
id: chatSessionState.id,
|
|
310
493
|
title: chatSessionState.title,
|
|
311
494
|
timestamp: Date.now(),
|
|
312
|
-
history: chat.getRawHistory()
|
|
313
|
-
})
|
|
314
|
-
}
|
|
495
|
+
history: chat.getRawHistory(),
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
catch (e) {
|
|
499
|
+
debugLog('Failed to save session with title: ' + e);
|
|
500
|
+
}
|
|
315
501
|
}
|
|
316
|
-
chatHistoryService
|
|
502
|
+
chatHistoryService
|
|
503
|
+
.saveSession({
|
|
317
504
|
id: chatSessionState.id,
|
|
318
505
|
title: chatSessionState.title,
|
|
319
506
|
timestamp: Date.now(),
|
|
320
|
-
history
|
|
321
|
-
})
|
|
507
|
+
history,
|
|
508
|
+
})
|
|
509
|
+
.catch((e) => debugLog('Failed to auto-save session: ' + e));
|
|
510
|
+
}
|
|
511
|
+
let planModeReturn = undefined;
|
|
512
|
+
if (isPlanMode && finalText && finalText !== '[Generation stopped.]') {
|
|
513
|
+
inputHandler.stop(); // MUST stop the async input handler before showing an interactive prompt, or it will intercept stdin and freeze/duplicate
|
|
514
|
+
// Ensure the terminal has fully scrolled and there is a clean line break
|
|
515
|
+
console.log('\n');
|
|
516
|
+
// Small delay to allow stdout to flush and terminal to finish scrolling from the markdown output
|
|
517
|
+
// This prevents @clack/prompts from miscalculating the cursor position and duplicating the prompt on arrow keys
|
|
518
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
519
|
+
const planAction = await p['select']({
|
|
520
|
+
message: 'Plan generated. What would you like to do?',
|
|
521
|
+
options: [
|
|
522
|
+
{ value: 'proceed', label: 'Proceed with plan', hint: 'Executes the generated plan immediately' },
|
|
523
|
+
{ value: 'edit', label: 'Edit plan', hint: 'Provide additional feedback to revise the plan' },
|
|
524
|
+
{ value: 'exit', label: 'Exit plan mode', hint: 'Return to normal chat without executing' },
|
|
525
|
+
],
|
|
526
|
+
});
|
|
527
|
+
if (p.isCancel(planAction) || planAction === 'exit') {
|
|
528
|
+
planModeReturn = 'exit';
|
|
529
|
+
}
|
|
530
|
+
else if (planAction === 'edit') {
|
|
531
|
+
planModeReturn = 'edit';
|
|
532
|
+
}
|
|
533
|
+
else if (planAction === 'proceed') {
|
|
534
|
+
planModeReturn = 'proceed';
|
|
535
|
+
}
|
|
322
536
|
}
|
|
537
|
+
return { planModeReturn, contextResult: gatherRes.contextResult };
|
|
323
538
|
}
|
|
324
539
|
catch (err) {
|
|
325
|
-
spinner.stop('');
|
|
326
540
|
const message = err instanceof Error ? err.message : String(err);
|
|
327
541
|
p.log.error(`${pc.red('Error:')} ${message}`);
|
|
328
542
|
}
|
|
329
543
|
finally {
|
|
544
|
+
spinner.stop();
|
|
545
|
+
process.stdout.write('\x1b[2K\r');
|
|
330
546
|
// Persist any verified or partial changes into our session change ledger.
|
|
331
547
|
// This ensures that if the process crashes, errors out, or is aborted mid-generation,
|
|
332
548
|
// the user can still use /revert to undo the partial file mutations.
|
|
549
|
+
const changedFiles = changeLogger.getChangedFiles();
|
|
333
550
|
changeLogger.commitChangeSet();
|
|
551
|
+
// Delta-update the embedding index if there were changes
|
|
552
|
+
if (changedFiles.length > 0) {
|
|
553
|
+
try {
|
|
554
|
+
const { getEmbeddingIndex } = await import('./embeddingIndex.js');
|
|
555
|
+
const index = getEmbeddingIndex();
|
|
556
|
+
// Only update if it's already loaded in memory
|
|
557
|
+
if (index.isReady()) {
|
|
558
|
+
await index.updateIndex(workspaceRoot, changedFiles);
|
|
559
|
+
await index.save(workspaceRoot);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
catch (e) {
|
|
563
|
+
debugLog(`Failed to delta-update embedding index: ${e.message}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
334
566
|
}
|
|
335
|
-
}
|
|
567
|
+
});
|
|
336
568
|
}
|
|
337
|
-
async function collectUserInput() {
|
|
569
|
+
async function collectUserInput(history, isPlanMode) {
|
|
338
570
|
let lines = [];
|
|
339
571
|
let isMultiLine = false;
|
|
340
572
|
while (true) {
|
|
341
|
-
const userInputRaw = await
|
|
342
|
-
message: isMultiLine ? ' ' : pc.magenta('❯'),
|
|
573
|
+
const userInputRaw = await historyText({
|
|
574
|
+
message: isMultiLine ? ' ' : isPlanMode ? pc.cyan('(Plan Mode) ❯') : pc.magenta('❯'),
|
|
343
575
|
placeholder: isMultiLine ? '' : 'Use "\\" for new lines',
|
|
576
|
+
history,
|
|
344
577
|
});
|
|
345
578
|
if (p.isCancel(userInputRaw)) {
|
|
346
579
|
if (isMultiLine) {
|
|
@@ -376,10 +609,12 @@ async function compressContextFiles(workspaceRoot, contextResult) {
|
|
|
376
609
|
const cachedContext = readCache(workspaceRoot, 'context_cache.json') || {};
|
|
377
610
|
let cacheUpdated = false;
|
|
378
611
|
const compressedFiles = new Map();
|
|
379
|
-
for (const [filePath,
|
|
380
|
-
|
|
612
|
+
for (const [filePath, contentObj] of contextResult.relevantFiles.entries()) {
|
|
613
|
+
const content = contentObj.text;
|
|
614
|
+
const inlineData = contentObj.inlineData;
|
|
615
|
+
if (content.length < 2000 && !inlineData) {
|
|
381
616
|
debugLog(`Bypassing cache and compression for ${filePath} (length ${content.length} < 2000)`);
|
|
382
|
-
compressedFiles.set(filePath,
|
|
617
|
+
compressedFiles.set(filePath, contentObj);
|
|
383
618
|
continue;
|
|
384
619
|
}
|
|
385
620
|
const fileHash = crypto
|
|
@@ -388,13 +623,13 @@ async function compressContextFiles(workspaceRoot, contextResult) {
|
|
|
388
623
|
.digest('hex');
|
|
389
624
|
if (cachedContext[fileHash]) {
|
|
390
625
|
debugLog(`Context cache HIT for file ${filePath} (hash ${fileHash.substring(0, 8)})`);
|
|
391
|
-
compressedFiles.set(filePath, cachedContext[fileHash]);
|
|
626
|
+
compressedFiles.set(filePath, { text: cachedContext[fileHash] });
|
|
392
627
|
}
|
|
393
628
|
else {
|
|
394
629
|
debugLog(`Context cache MISS for file ${filePath} (hash ${fileHash.substring(0, 8)})`);
|
|
395
630
|
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);
|
|
631
|
+
const summary = await compressTextUsingFlashLite(content, compressPrompt, inlineData);
|
|
632
|
+
compressedFiles.set(filePath, { text: summary });
|
|
398
633
|
cachedContext[fileHash] = summary;
|
|
399
634
|
cacheUpdated = true;
|
|
400
635
|
}
|
|
@@ -441,8 +676,12 @@ async function executeSelfCorrectionLoop(chat, initialResult, workspaceRoot, inp
|
|
|
441
676
|
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
677
|
spinner.start('Thinking (Correction)...');
|
|
443
678
|
result = await chat.sendMessage(forcePrompt);
|
|
444
|
-
spinner.stop(
|
|
679
|
+
spinner.stop();
|
|
680
|
+
process.stdout.write('\x1b[2K\r');
|
|
445
681
|
correctionAttempts++;
|
|
682
|
+
const collector = getMetricCollector();
|
|
683
|
+
if (collector)
|
|
684
|
+
collector.recordSelfCorrection();
|
|
446
685
|
continue;
|
|
447
686
|
}
|
|
448
687
|
previousChangeCount = currentChanges.length;
|
|
@@ -466,9 +705,17 @@ async function executeSelfCorrectionLoop(chat, initialResult, workspaceRoot, inp
|
|
|
466
705
|
}
|
|
467
706
|
if (!hasErrors) {
|
|
468
707
|
p.log.success('Verification passed.');
|
|
708
|
+
const collector = getMetricCollector();
|
|
709
|
+
if (collector && correctionAttempts === 0)
|
|
710
|
+
collector.recordVerificationResult(true);
|
|
469
711
|
break;
|
|
470
712
|
}
|
|
713
|
+
const collector = getMetricCollector();
|
|
714
|
+
if (collector && correctionAttempts === 0)
|
|
715
|
+
collector.recordVerificationResult(false);
|
|
471
716
|
correctionAttempts++;
|
|
717
|
+
if (collector)
|
|
718
|
+
collector.recordSelfCorrection();
|
|
472
719
|
if (correctionAttempts > MAX_CORRECTIONS) {
|
|
473
720
|
p.log.warn('Max self-correction attempts reached. Leaving remaining issues for manual review.');
|
|
474
721
|
break;
|
|
@@ -496,7 +743,8 @@ async function executeSelfCorrectionLoop(chat, initialResult, workspaceRoot, inp
|
|
|
496
743
|
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
744
|
spinner.start('Thinking (Correction)...');
|
|
498
745
|
result = await chat.sendMessage(correctionPrompt);
|
|
499
|
-
spinner.stop(
|
|
746
|
+
spinner.stop();
|
|
747
|
+
process.stdout.write('\x1b[2K\r');
|
|
500
748
|
}
|
|
501
749
|
return { finalText };
|
|
502
750
|
}
|
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.
|