minovative-mind-cli 2.3.3 → 2.5.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 +5 -0
- package/dist/commands/chat.js +2 -1
- package/dist/services/agent/slashCommands.js +152 -2
- package/dist/services/agent/toolLoop.js +5 -17
- package/dist/services/agent-tools.js +26 -14
- package/dist/services/agent.js +43 -13
- package/dist/services/ai.d.ts +2 -2
- package/dist/services/ai.js +131 -14
- package/dist/services/changeLogger.js +2 -2
- package/dist/services/chatHistoryService.d.ts +8 -0
- package/dist/services/chatHistoryService.js +24 -11
- package/dist/services/contextAgent.d.ts +1 -0
- package/dist/services/contextAgent.js +57 -32
- package/dist/services/metrics.d.ts +21 -4
- package/dist/services/metrics.js +18 -0
- package/dist/services/orchestration/investigationAgent.js +0 -32
- package/dist/services/orchestration/investigationCache.d.ts +25 -0
- package/dist/services/orchestration/investigationCache.js +135 -0
- package/dist/services/orchestration/investigationOrchestrator.js +5 -1
- package/dist/services/orchestration/messageBus.d.ts +1 -1
- package/dist/services/orchestration/messageBus.js +14 -14
- package/dist/services/orchestration/orchestrator.js +1 -1
- package/dist/services/orchestration/readCache.js +4 -0
- package/dist/services/orchestration/scopedTools.js +2 -1
- package/dist/services/orchestration/subAgent.js +10 -26
- package/dist/services/proxyClient.d.ts +8 -0
- package/dist/services/proxyClient.js +17 -0
- package/dist/services/verificationService.js +15 -6
- package/dist/utils/config.d.ts +4 -0
- package/dist/utils/config.js +8 -0
- package/dist/utils/credentialStore.d.ts +7 -0
- package/dist/utils/credentialStore.js +8 -0
- package/dist/utils/projectStorage.js +7 -0
- package/dist/utils/syntaxValidator.js +125 -21
- package/dist/utils/systemPrompts.d.ts +2 -2
- package/dist/utils/systemPrompts.js +3 -3
- package/oclif.manifest.json +2 -2
- package/package.json +1 -2
package/dist/services/ai.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
2
|
+
import { GEMINI_MODELS, DEFAULT_MODEL, MAX_OUTPUT_TOKENS, isByokEnabled } from '../utils/config.js';
|
|
2
3
|
import { getToolDeclarations } from './agent-tools.js';
|
|
3
|
-
import {
|
|
4
|
+
import { getMetricCollector } from './metrics.js';
|
|
4
5
|
import { getAuthorizedIdToken } from './auth.js';
|
|
5
6
|
import { debugLog } from '../utils/logger.js';
|
|
6
7
|
import { readCache, writeCache } from '../utils/projectStorage.js';
|
|
7
8
|
import { GENERAL_CHAT_INSTRUCTION, PLAN_EXECUTION_INSTRUCTION, PLAN_MODE_INSTRUCTION, CONTEXT_SYSTEM_INSTRUCTION, INTENT_ROUTER_SYSTEM_INSTRUCTION, WEB_SEARCH_SYSTEM_INSTRUCTION, EXECUTION_COMPLEXITY_SYSTEM_INSTRUCTION, INVESTIGATION_COMPLEXITY_SYSTEM_INSTRUCTION, } from '../utils/systemPrompts.js';
|
|
8
|
-
import { getMetricCollector } from './metrics.js';
|
|
9
9
|
import { workspaceRegistry } from './workspaceRegistry.js';
|
|
10
|
+
import { loadCredentials } from '../utils/credentialStore.js';
|
|
11
|
+
import { ProxyClient, accumulateTurnUsage } from './proxyClient.js';
|
|
10
12
|
function getMultiWorkspaceBlock() {
|
|
11
13
|
const summary = workspaceRegistry.buildPromptSummary();
|
|
12
14
|
if (!summary)
|
|
@@ -206,13 +208,57 @@ export class ProxyChatSession {
|
|
|
206
208
|
// Prune old history before sending to keep payload bounded
|
|
207
209
|
await this.pruneHistory();
|
|
208
210
|
const effectiveGenerationConfig = { ...this.generationConfig };
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
211
|
+
const byokEnabled = await isByokEnabled();
|
|
212
|
+
let result;
|
|
213
|
+
if (byokEnabled) {
|
|
214
|
+
const creds = await loadCredentials();
|
|
215
|
+
const genAI = new GoogleGenerativeAI(creds.geminiApiKey);
|
|
216
|
+
const model = genAI.getGenerativeModel({
|
|
217
|
+
model: this.modelName,
|
|
218
|
+
systemInstruction: this.systemInstruction,
|
|
219
|
+
tools: this.tools,
|
|
220
|
+
});
|
|
221
|
+
const chat = model.startChat({
|
|
222
|
+
history: this.history.slice(0, -1),
|
|
223
|
+
generationConfig: effectiveGenerationConfig,
|
|
224
|
+
});
|
|
225
|
+
const lastMessage = this.history[this.history.length - 1];
|
|
226
|
+
try {
|
|
227
|
+
const response = await chat.sendMessage(lastMessage.parts);
|
|
228
|
+
const responseObj = await response.response;
|
|
229
|
+
result = {
|
|
230
|
+
functionCalls: responseObj.functionCalls(),
|
|
231
|
+
parts: responseObj.candidates?.[0]?.content?.parts,
|
|
232
|
+
usageMetadata: responseObj.usageMetadata,
|
|
233
|
+
};
|
|
234
|
+
if (responseObj.usageMetadata) {
|
|
235
|
+
const collector = getMetricCollector();
|
|
236
|
+
collector?.accumulateUsage(responseObj.usageMetadata);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
debugLog(`Failed to send message via BYOK: ${error}`);
|
|
241
|
+
const errorMessage = error?.message || '';
|
|
242
|
+
if (error?.status === 401 ||
|
|
243
|
+
error?.status === 403 ||
|
|
244
|
+
errorMessage.includes('API_KEY_INVALID') ||
|
|
245
|
+
errorMessage.includes('quota') ||
|
|
246
|
+
errorMessage.includes('PERMISSION_DENIED')) {
|
|
247
|
+
throw new Error('AI_BYOK_ERROR: Your API key or quota is invalid. Please run /config-key to update your settings.');
|
|
248
|
+
}
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
result = await proxyClient.generateFunctionCallViaProxy(idToken, this.modelName, this.history, this.tools, undefined, // toolConfig
|
|
254
|
+
this.systemInstruction, effectiveGenerationConfig, onChunk ? { onChunk } : undefined, // streamCallbacks
|
|
255
|
+
abortSignal);
|
|
256
|
+
}
|
|
212
257
|
this.latestUsageMetadata = result.usageMetadata;
|
|
213
258
|
// Track token usage metrics
|
|
214
259
|
if (result.usageMetadata) {
|
|
215
260
|
const collector = getMetricCollector();
|
|
261
|
+
collector?.accumulateUsage(result.usageMetadata);
|
|
216
262
|
if (collector) {
|
|
217
263
|
collector.recordTokenUsage(result.usageMetadata.promptTokens || 0, result.usageMetadata.candidatesTokens || 0, result.usageMetadata.cachedTokens || 0);
|
|
218
264
|
}
|
|
@@ -306,9 +352,9 @@ export function getPlanModeConfig() {
|
|
|
306
352
|
* Compresses a large string of text using gemini-3.5-flash-lite.
|
|
307
353
|
* Used for shrinking context payloads to prevent OOM/choking.
|
|
308
354
|
*/
|
|
309
|
-
export async function compressTextUsingFlashLite(text, instruction = "<directives>\nSummarize the following text concisely. Preserve the most critical technical details, function names, and architecture logic. Make sure it's understandable without the fluff.\n</directives>", inlineData) {
|
|
310
|
-
if (!text || (text.length < 1000 && !inlineData))
|
|
311
|
-
return text; // Don't compress tiny texts
|
|
355
|
+
export async function compressTextUsingFlashLite(text, instruction = "<directives>\nSummarize the following text concisely. Preserve the most critical technical details, function names, and architecture logic. Make sure it's understandable without the fluff.\n</directives>", inlineData, force = false) {
|
|
356
|
+
if (!text || (!force && text.length < 1000 && !inlineData))
|
|
357
|
+
return text; // Don't compress tiny texts unless forced
|
|
312
358
|
try {
|
|
313
359
|
const idToken = await getAuthorizedIdToken();
|
|
314
360
|
if (!idToken)
|
|
@@ -321,8 +367,38 @@ export async function compressTextUsingFlashLite(text, instruction = "<directive
|
|
|
321
367
|
parts.push({ inlineData });
|
|
322
368
|
}
|
|
323
369
|
const contents = [{ role: 'user', parts }];
|
|
324
|
-
const
|
|
325
|
-
|
|
370
|
+
const byokEnabled = await isByokEnabled();
|
|
371
|
+
let result;
|
|
372
|
+
if (byokEnabled) {
|
|
373
|
+
try {
|
|
374
|
+
const creds = await loadCredentials();
|
|
375
|
+
const genAI = new GoogleGenerativeAI(creds.geminiApiKey);
|
|
376
|
+
const modelObj = genAI.getGenerativeModel({
|
|
377
|
+
model,
|
|
378
|
+
systemInstruction: instruction,
|
|
379
|
+
});
|
|
380
|
+
const response = await modelObj.generateContent({
|
|
381
|
+
contents: [{ role: 'user', parts }],
|
|
382
|
+
generationConfig: { temperature: 0.2 },
|
|
383
|
+
});
|
|
384
|
+
const responseObj = await response.response;
|
|
385
|
+
result = {
|
|
386
|
+
parts: responseObj.candidates?.[0]?.content?.parts,
|
|
387
|
+
usageMetadata: responseObj.usageMetadata,
|
|
388
|
+
};
|
|
389
|
+
if (responseObj.usageMetadata) {
|
|
390
|
+
accumulateTurnUsage(responseObj.usageMetadata, model);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
console.error('BYOK Error:', error.message);
|
|
395
|
+
throw new Error(`BYOK AI Error: ${error.message}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
result = await proxyClient.generateFunctionCallViaProxy(idToken, model, contents, [], // no tools
|
|
400
|
+
undefined, instruction, { temperature: 0.2 });
|
|
401
|
+
}
|
|
326
402
|
let textPart = '';
|
|
327
403
|
if (result.parts) {
|
|
328
404
|
// Use highly optimized local loop instead of array search to avoid callback allocation and naive database regex flags
|
|
@@ -338,6 +414,9 @@ export async function compressTextUsingFlashLite(text, instruction = "<directive
|
|
|
338
414
|
}
|
|
339
415
|
catch (error) {
|
|
340
416
|
debugLog(`Failed to compress text using flash-lite: ${error}`);
|
|
417
|
+
if (error?.status === 401 || error?.status === 403 || error?.message?.includes('API_KEY_INVALID') || error?.message?.includes('quota')) {
|
|
418
|
+
throw new Error('AI_AUTH_ERROR: Your API key or quota is invalid. Please run /config-key or re-login.');
|
|
419
|
+
}
|
|
341
420
|
return text; // fallback to raw text if compression fails
|
|
342
421
|
}
|
|
343
422
|
}
|
|
@@ -581,8 +660,38 @@ export async function generateChatTitle(firstMessage) {
|
|
|
581
660
|
let model = getGlobalActiveModel();
|
|
582
661
|
if (model === 'auto')
|
|
583
662
|
model = GEMINI_MODELS.FLASH_LITE;
|
|
584
|
-
const
|
|
585
|
-
|
|
663
|
+
const byokEnabled = await isByokEnabled();
|
|
664
|
+
let result;
|
|
665
|
+
if (byokEnabled) {
|
|
666
|
+
try {
|
|
667
|
+
const creds = await loadCredentials();
|
|
668
|
+
const genAI = new GoogleGenerativeAI(creds.geminiApiKey);
|
|
669
|
+
const modelObj = genAI.getGenerativeModel({
|
|
670
|
+
model,
|
|
671
|
+
systemInstruction: instruction,
|
|
672
|
+
});
|
|
673
|
+
const response = await modelObj.generateContent({
|
|
674
|
+
contents,
|
|
675
|
+
generationConfig: { temperature: 0.2 },
|
|
676
|
+
});
|
|
677
|
+
const responseObj = await response.response;
|
|
678
|
+
result = {
|
|
679
|
+
parts: responseObj.candidates?.[0]?.content?.parts,
|
|
680
|
+
usageMetadata: responseObj.usageMetadata,
|
|
681
|
+
};
|
|
682
|
+
if (responseObj.usageMetadata) {
|
|
683
|
+
accumulateTurnUsage(responseObj.usageMetadata, model);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
console.error('BYOK Error:', error.message);
|
|
688
|
+
throw new Error(`BYOK AI Error: ${error.message}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
result = await proxyClient.generateFunctionCallViaProxy(idToken, model, contents, [], // no tools
|
|
693
|
+
undefined, instruction, { temperature: 0.2 });
|
|
694
|
+
}
|
|
586
695
|
let title = '';
|
|
587
696
|
if (result.parts) {
|
|
588
697
|
for (const p of result.parts) {
|
|
@@ -597,7 +706,15 @@ export async function generateChatTitle(firstMessage) {
|
|
|
597
706
|
return title ? title : firstMessage.substring(0, maxLength);
|
|
598
707
|
}
|
|
599
708
|
catch (error) {
|
|
600
|
-
debugLog(`Failed to generate chat title: ${error}`);
|
|
709
|
+
debugLog(`Failed to generate chat title via BYOK: ${error}`);
|
|
710
|
+
const errorMessage = error?.message || '';
|
|
711
|
+
if (error?.status === 401 ||
|
|
712
|
+
error?.status === 403 ||
|
|
713
|
+
errorMessage.includes('API_KEY_INVALID') ||
|
|
714
|
+
errorMessage.includes('quota') ||
|
|
715
|
+
errorMessage.includes('PERMISSION_DENIED')) {
|
|
716
|
+
throw new Error('AI_BYOK_ERROR: Your API key or quota is invalid. Please run /config-key to update your settings.');
|
|
717
|
+
}
|
|
601
718
|
return firstMessage.substring(0, maxLength);
|
|
602
719
|
}
|
|
603
720
|
}
|
|
@@ -21,7 +21,7 @@ class ChangeLogger {
|
|
|
21
21
|
* Maximum number of historical changesets retained in memory and on disk.
|
|
22
22
|
* Older changesets are discarded on save once this limit is exceeded.
|
|
23
23
|
*/
|
|
24
|
-
MAX_HISTORY =
|
|
24
|
+
MAX_HISTORY = 20;
|
|
25
25
|
/**
|
|
26
26
|
* Whether the change logger is currently enabled.
|
|
27
27
|
*/
|
|
@@ -66,7 +66,7 @@ class ChangeLogger {
|
|
|
66
66
|
}
|
|
67
67
|
writeCache(this.workspaceRoot, 'revert_state.json', {
|
|
68
68
|
history: this.changeStack,
|
|
69
|
-
isEnabled: this.isEnabled
|
|
69
|
+
isEnabled: this.isEnabled,
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
@@ -80,6 +80,14 @@ declare class ChatHistoryService {
|
|
|
80
80
|
* @returns A promise that resolves when the session and its associated archive have been deleted.
|
|
81
81
|
*/
|
|
82
82
|
deleteSession(id: string): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Deletes multiple chat sessions from the local workspace cache and removes their associated archived history files.
|
|
85
|
+
* If any session or its archived history file does not exist, the operation continues for the remaining sessions.
|
|
86
|
+
*
|
|
87
|
+
* @param ids - An array of unique identifiers of the chat sessions to delete.
|
|
88
|
+
* @returns A promise that resolves when the sessions and their associated archives have been deleted.
|
|
89
|
+
*/
|
|
90
|
+
bulkDeleteSessions(ids: string[]): Promise<void>;
|
|
83
91
|
}
|
|
84
92
|
/**
|
|
85
93
|
* Singleton instance of the `ChatHistoryService` exported for application-wide use.
|
|
@@ -64,23 +64,36 @@ class ChatHistoryService {
|
|
|
64
64
|
* @returns A promise that resolves when the session and its associated archive have been deleted.
|
|
65
65
|
*/
|
|
66
66
|
async deleteSession(id) {
|
|
67
|
-
|
|
67
|
+
await this.bulkDeleteSessions([id]);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Deletes multiple chat sessions from the local workspace cache and removes their associated archived history files.
|
|
71
|
+
* If any session or its archived history file does not exist, the operation continues for the remaining sessions.
|
|
72
|
+
*
|
|
73
|
+
* @param ids - An array of unique identifiers of the chat sessions to delete.
|
|
74
|
+
* @returns A promise that resolves when the sessions and their associated archives have been deleted.
|
|
75
|
+
*/
|
|
76
|
+
async bulkDeleteSessions(ids) {
|
|
77
|
+
if (!this.workspaceRoot || ids.length === 0)
|
|
68
78
|
return;
|
|
69
79
|
let sessions = this.getSessions();
|
|
70
|
-
|
|
80
|
+
const idSet = new Set(ids);
|
|
81
|
+
sessions = sessions.filter((s) => !idSet.has(s.id));
|
|
71
82
|
await writeCache(this.workspaceRoot, 'chat_sessions.json', sessions);
|
|
72
|
-
// Also delete the archived
|
|
83
|
+
// Also delete the archived histories if they exist
|
|
73
84
|
const { promises: fs } = await import('node:fs');
|
|
74
85
|
const path = await import('node:path');
|
|
75
86
|
const { getProjectStorageDir } = await import('../utils/projectStorage.js');
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
const storageDir = getProjectStorageDir(this.workspaceRoot);
|
|
88
|
+
await Promise.all(ids.map(async (id) => {
|
|
89
|
+
try {
|
|
90
|
+
const archivePath = path.join(storageDir, `archived_history_${id}.json`);
|
|
91
|
+
await fs.unlink(archivePath);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Ignore error if file doesn't exist
|
|
95
|
+
}
|
|
96
|
+
}));
|
|
84
97
|
}
|
|
85
98
|
}
|
|
86
99
|
/**
|
|
@@ -210,6 +210,57 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
210
210
|
projectTree += `=== ${label} ===\\nProject Type: ${type}\\n${tree}\\n\\n`;
|
|
211
211
|
}
|
|
212
212
|
const projectType = primaryProjectType;
|
|
213
|
+
const { lookupInvestigation, saveInvestigation } = await import('./orchestration/investigationCache.js');
|
|
214
|
+
const cacheHit = await lookupInvestigation(workspaceRoot, userRequest);
|
|
215
|
+
if (cacheHit) {
|
|
216
|
+
if (onProgress)
|
|
217
|
+
onProgress(`⚡ Memory Bank HIT — loaded ${cacheHit.entry.relevantFiles.length} files from cache`);
|
|
218
|
+
const cachedFiles = new Map();
|
|
219
|
+
for (const filePath of cacheHit.entry.relevantFiles) {
|
|
220
|
+
const readResult = await executeTool(workspaceRoot, 'read_file', { filePath });
|
|
221
|
+
if (!readResult.error) {
|
|
222
|
+
cachedFiles.set(filePath, { text: readResult.output, inlineData: readResult.inlineData });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const MAX_TOTAL_FILES = 30;
|
|
226
|
+
try {
|
|
227
|
+
const { resolveAndValidateMultiWorkspacePath } = await import('../utils/pathSecurity.js');
|
|
228
|
+
const autoDiscovered = new Set();
|
|
229
|
+
for (const filePath of cacheHit.entry.relevantFiles) {
|
|
230
|
+
try {
|
|
231
|
+
const resolved = resolveAndValidateMultiWorkspacePath(workspaceRoot, filePath);
|
|
232
|
+
const graph = await buildDependencyGraph(resolved.workspaceRoot);
|
|
233
|
+
const reverseDeps = graph.getImportedBy(resolved.relativePath);
|
|
234
|
+
for (const dep of reverseDeps) {
|
|
235
|
+
if (cachedFiles.has(dep) || autoDiscovered.has(dep))
|
|
236
|
+
continue;
|
|
237
|
+
if (cachedFiles.size + autoDiscovered.size >= MAX_TOTAL_FILES)
|
|
238
|
+
break;
|
|
239
|
+
autoDiscovered.add(dep);
|
|
240
|
+
const depReadRes = await executeTool(resolved.workspaceRoot, 'read_file', { filePath: dep });
|
|
241
|
+
if (!depReadRes.error) {
|
|
242
|
+
cachedFiles.set(dep, { text: depReadRes.output, inlineData: depReadRes.inlineData });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch (e) { }
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch (e) { }
|
|
250
|
+
const { accumulateUsage } = await import('./metrics.js');
|
|
251
|
+
accumulateUsage({ cachedTokens: cacheHit.bytesSaved }); // Rough proxy for saved tokens
|
|
252
|
+
return {
|
|
253
|
+
contextResult: {
|
|
254
|
+
projectTree,
|
|
255
|
+
projectType,
|
|
256
|
+
summary: cacheHit.entry.summary,
|
|
257
|
+
relevantFiles: cachedFiles,
|
|
258
|
+
fromMemoryBank: true,
|
|
259
|
+
},
|
|
260
|
+
targetAgent,
|
|
261
|
+
chainedMessages: [],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
213
264
|
// ─── Parallel Investigation Gate ─────────────────────────────
|
|
214
265
|
if (isSubAgentsEnabled()) {
|
|
215
266
|
// Determine complexity and domain breakdown
|
|
@@ -231,9 +282,6 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
231
282
|
const relevantFiles = new Map();
|
|
232
283
|
let summary = 'No relevant context found.';
|
|
233
284
|
let isInvestigationFinished = false;
|
|
234
|
-
const visitedToolCalls = new Set();
|
|
235
|
-
let consecutiveDuplicates = 0;
|
|
236
|
-
const MAX_CONSECUTIVE_DUPLICATES = 3;
|
|
237
285
|
let chainedMessages = [];
|
|
238
286
|
let webSearchSummary = '';
|
|
239
287
|
// Initial prompt
|
|
@@ -285,33 +333,6 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
285
333
|
if (abortSignal.aborted)
|
|
286
334
|
break;
|
|
287
335
|
const args = call.args;
|
|
288
|
-
// Deduplicate identical tool calls
|
|
289
|
-
const callSignature = `${call.name}:${JSON.stringify(args)}`;
|
|
290
|
-
if (call.name !== 'finish_investigation' && visitedToolCalls.has(callSignature)) {
|
|
291
|
-
consecutiveDuplicates++;
|
|
292
|
-
if (consecutiveDuplicates >= MAX_CONSECUTIVE_DUPLICATES) {
|
|
293
|
-
// Force-finish: the model is stuck in a loop, finalize with whatever context we have
|
|
294
|
-
const forceMsg = `Investigation auto-completed: the model repeated the same tool call ${MAX_CONSECUTIVE_DUPLICATES} times consecutively.`;
|
|
295
|
-
debugLog(`[Context Agent] ${forceMsg}`);
|
|
296
|
-
if (onProgress)
|
|
297
|
-
onProgress(forceMsg);
|
|
298
|
-
if (!summary || summary === 'No relevant context found.') {
|
|
299
|
-
summary = 'Investigation was auto-completed due to repeated duplicate tool calls. Review the gathered files for context.';
|
|
300
|
-
}
|
|
301
|
-
isFinished = true;
|
|
302
|
-
isInvestigationFinished = relevantFiles.size > 0;
|
|
303
|
-
break;
|
|
304
|
-
}
|
|
305
|
-
functionResponses.push({
|
|
306
|
-
functionResponse: {
|
|
307
|
-
name: call.name,
|
|
308
|
-
response: { error: `DUPLICATE CALL BLOCKED (attempt ${consecutiveDuplicates}/${MAX_CONSECUTIVE_DUPLICATES}): You already executed this exact tool call. Do NOT retry it. Use the results you already have and call finish_investigation now, or try a DIFFERENT tool call with different parameters.` }
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
consecutiveDuplicates = 0;
|
|
314
|
-
visitedToolCalls.add(callSignature);
|
|
315
336
|
let logMsg = ` [Context Agent] Executing ${call.name}`;
|
|
316
337
|
if (call.name === 'finish_investigation') {
|
|
317
338
|
const filesToRead = args.relevantFiles || [];
|
|
@@ -369,7 +390,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
369
390
|
// When the Context Agent finalizes its investigation, we automatically
|
|
370
391
|
// discover files that DEPEND ON the selected files. This ensures the
|
|
371
392
|
// Execution Agent won't break imports when modifying/deleting/renaming.
|
|
372
|
-
const MAX_TOTAL_FILES =
|
|
393
|
+
const MAX_TOTAL_FILES = 30;
|
|
373
394
|
try {
|
|
374
395
|
const { resolveAndValidateMultiWorkspacePath } = await import('../utils/pathSecurity.js');
|
|
375
396
|
const autoDiscovered = new Set();
|
|
@@ -548,9 +569,13 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
548
569
|
if (collector) {
|
|
549
570
|
collector.recordContextSelectedFiles(Array.from(relevantFiles.keys()));
|
|
550
571
|
if (!isInvestigationFinished || relevantFiles.size === 0) {
|
|
551
|
-
collector
|
|
572
|
+
collector?.recordInvestigationFailure?.();
|
|
552
573
|
}
|
|
553
574
|
}
|
|
575
|
+
if (isInvestigationFinished && relevantFiles.size > 0) {
|
|
576
|
+
const { saveInvestigation } = await import('./orchestration/investigationCache.js');
|
|
577
|
+
await saveInvestigation(workspaceRoot, userRequest, Array.from(relevantFiles.keys()), summary);
|
|
578
|
+
}
|
|
554
579
|
return {
|
|
555
580
|
contextResult: { projectTree, projectType, relevantFiles, summary, webSearchSummary, isParallel: false },
|
|
556
581
|
targetAgent,
|
|
@@ -2,6 +2,13 @@ export interface MetricCollector {
|
|
|
2
2
|
startTimer(name: string): void;
|
|
3
3
|
stopTimer(name: string): number;
|
|
4
4
|
recordTokenUsage(input: number, output: number, cached?: number): void;
|
|
5
|
+
accumulateUsage(usage: any): void;
|
|
6
|
+
resetTurnAccumulator(): void;
|
|
7
|
+
getTurnTotals(): {
|
|
8
|
+
promptTokens: number;
|
|
9
|
+
outputTokens: number;
|
|
10
|
+
cachedTokens: number;
|
|
11
|
+
};
|
|
5
12
|
recordCompressedContextSize(chars: number): void;
|
|
6
13
|
recordContextSelectedFiles(files: string[]): void;
|
|
7
14
|
recordFileModified(filePath: string): void;
|
|
@@ -9,10 +16,20 @@ export interface MetricCollector {
|
|
|
9
16
|
recordSelfCorrection(): void;
|
|
10
17
|
recordVerificationResult(passed: boolean): void;
|
|
11
18
|
recordMatchTier(tier: 'exact' | 'normalized' | 'levenshtein' | 'none'): void;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
recordWriteFailure(): void;
|
|
19
|
+
recordCacheHit(cacheType?: 'investigation' | 'read'): void;
|
|
20
|
+
recordCacheMiss(cacheType?: 'investigation' | 'read'): void;
|
|
21
|
+
recordCachePerformance(cacheType: 'investigation' | 'read', durationMs: number): void;
|
|
22
|
+
recordWriteFailure?(): void;
|
|
23
|
+
recordModifyFailure?(): void;
|
|
24
|
+
recordToolFailure?(toolName?: string): void;
|
|
25
|
+
recordInvestigationFailure?(): void;
|
|
16
26
|
}
|
|
17
27
|
export declare function setMetricCollector(collector: MetricCollector | null): void;
|
|
18
28
|
export declare function getMetricCollector(): MetricCollector | null;
|
|
29
|
+
export declare function resetTurnAccumulator(): void;
|
|
30
|
+
export declare function accumulateUsage(usage: any): void;
|
|
31
|
+
export declare function getTurnTotals(): {
|
|
32
|
+
promptTokens: number;
|
|
33
|
+
outputTokens: number;
|
|
34
|
+
cachedTokens: number;
|
|
35
|
+
};
|
package/dist/services/metrics.js
CHANGED
|
@@ -5,3 +5,21 @@ export function setMetricCollector(collector) {
|
|
|
5
5
|
export function getMetricCollector() {
|
|
6
6
|
return globalCollector;
|
|
7
7
|
}
|
|
8
|
+
let turnPromptTokens = 0;
|
|
9
|
+
let turnOutputTokens = 0;
|
|
10
|
+
let turnCachedTokens = 0;
|
|
11
|
+
export function resetTurnAccumulator() {
|
|
12
|
+
turnPromptTokens = 0;
|
|
13
|
+
turnOutputTokens = 0;
|
|
14
|
+
turnCachedTokens = 0;
|
|
15
|
+
}
|
|
16
|
+
export function accumulateUsage(usage) {
|
|
17
|
+
if (!usage)
|
|
18
|
+
return;
|
|
19
|
+
turnPromptTokens += usage.promptTokens || 0;
|
|
20
|
+
turnOutputTokens += usage.candidatesTokens || 0;
|
|
21
|
+
turnCachedTokens += usage.cachedTokens || 0;
|
|
22
|
+
}
|
|
23
|
+
export function getTurnTotals() {
|
|
24
|
+
return { promptTokens: turnPromptTokens, outputTokens: turnOutputTokens, cachedTokens: turnCachedTokens };
|
|
25
|
+
}
|
|
@@ -113,9 +113,6 @@ export class InvestigationAgentRunner {
|
|
|
113
113
|
}
|
|
114
114
|
currentMessage += `\n\nStart investigating to find relevant files within your assigned domains.`;
|
|
115
115
|
let isFinished = false;
|
|
116
|
-
const visitedToolCalls = new Set();
|
|
117
|
-
let consecutiveDuplicates = 0;
|
|
118
|
-
const MAX_CONSECUTIVE_DUPLICATES = 3;
|
|
119
116
|
// Tool loop — mirrors the existing context agent loop in contextAgent.ts
|
|
120
117
|
while (!crashed && !abortSignal.aborted && !isFinished) {
|
|
121
118
|
this.pingHeartbeat();
|
|
@@ -143,35 +140,6 @@ export class InvestigationAgentRunner {
|
|
|
143
140
|
this.pingHeartbeat();
|
|
144
141
|
const args = call.args;
|
|
145
142
|
const logPrefix = `[${this.agentLabel}]`;
|
|
146
|
-
// Deduplicate identical tool calls
|
|
147
|
-
const callSignature = `${call.name}:${JSON.stringify(args)}`;
|
|
148
|
-
if (call.name !== 'finish_investigation' && visitedToolCalls.has(callSignature)) {
|
|
149
|
-
consecutiveDuplicates++;
|
|
150
|
-
if (consecutiveDuplicates >= MAX_CONSECUTIVE_DUPLICATES) {
|
|
151
|
-
// Force-finish: the model is stuck in a loop
|
|
152
|
-
const forceMsg = `Investigation auto-completed: the model repeated the same tool call ${MAX_CONSECUTIVE_DUPLICATES} times consecutively.`;
|
|
153
|
-
debugLog(`InvestigationAgent [${this.agentLabel}]: ${forceMsg}`);
|
|
154
|
-
if (onProgress)
|
|
155
|
-
onProgress(`[${this.agentLabel}] ${forceMsg}`);
|
|
156
|
-
if (!summary || summary === 'No relevant context found.') {
|
|
157
|
-
summary = 'Investigation was auto-completed due to repeated duplicate tool calls. Review the gathered files for context.';
|
|
158
|
-
}
|
|
159
|
-
isFinished = true;
|
|
160
|
-
success = relevantFiles.size > 0;
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
|
-
functionResponses.push({
|
|
164
|
-
functionResponse: {
|
|
165
|
-
name: call.name,
|
|
166
|
-
response: {
|
|
167
|
-
error: `DUPLICATE CALL BLOCKED (attempt ${consecutiveDuplicates}/${MAX_CONSECUTIVE_DUPLICATES}): You already executed this exact tool call. Do NOT retry it. Use the results you already have and call finish_investigation now, or try a DIFFERENT tool call with different parameters.`,
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
consecutiveDuplicates = 0;
|
|
174
|
-
visitedToolCalls.add(callSignature);
|
|
175
143
|
if (call.name === 'finish_investigation') {
|
|
176
144
|
summary = args.summary || '';
|
|
177
145
|
const filesToRead = args.relevantFiles || [];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface InvestigationCacheEntry {
|
|
2
|
+
promptHash: string;
|
|
3
|
+
relevantFiles: string[];
|
|
4
|
+
summary: string;
|
|
5
|
+
workspaceFingerprint: string;
|
|
6
|
+
createdAt: number;
|
|
7
|
+
}
|
|
8
|
+
export interface InvestigationCacheStore {
|
|
9
|
+
entries: Record<string, InvestigationCacheEntry>;
|
|
10
|
+
}
|
|
11
|
+
export declare function normalizePrompt(prompt: string): string;
|
|
12
|
+
export declare function hashPrompt(normalized: string): string;
|
|
13
|
+
export declare function generateWorkspaceFingerprint(workspaceRoot: string, relevantFiles: string[]): Promise<string>;
|
|
14
|
+
export declare function lookupInvestigation(workspaceRoot: string, userPrompt: string): Promise<{
|
|
15
|
+
entry: InvestigationCacheEntry;
|
|
16
|
+
bytesSaved: number;
|
|
17
|
+
} | null>;
|
|
18
|
+
export declare function saveInvestigation(workspaceRoot: string, userPrompt: string, relevantFiles: string[], summary: string): Promise<void>;
|
|
19
|
+
export declare function invalidateFilesFromInvestigationCache(workspaceRoot: string, changedFiles: string[]): Promise<void>;
|
|
20
|
+
export declare function getInvestigationCacheStats(workspaceRoot: string): {
|
|
21
|
+
entries: number;
|
|
22
|
+
sizeBytes: number;
|
|
23
|
+
hitCount: number;
|
|
24
|
+
missCount: number;
|
|
25
|
+
};
|