minovative-mind-cli 2.4.0 → 2.5.1
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 +1 -0
- package/dist/services/agent/slashCommands.js +106 -2
- package/dist/services/agent/syntaxAgent.d.ts +10 -0
- package/dist/services/agent/syntaxAgent.js +52 -0
- package/dist/services/agent/toolLoop.js +5 -17
- package/dist/services/agent-tools.js +89 -28
- package/dist/services/agent.js +1 -0
- package/dist/services/ai.d.ts +1 -1
- package/dist/services/ai.js +128 -13
- package/dist/services/changeLogger.js +2 -2
- package/dist/services/contextAgent.js +5 -35
- package/dist/services/metrics.d.ts +6 -8
- package/dist/services/orchestration/investigationAgent.js +0 -32
- package/dist/services/orchestration/investigationOrchestrator.js +1 -1
- package/dist/services/orchestration/messageBus.d.ts +1 -1
- package/dist/services/orchestration/messageBus.js +14 -14
- package/dist/services/orchestration/readCache.js +1 -1
- package/dist/services/orchestration/scopedTools.js +2 -1
- package/dist/services/orchestration/subAgent.js +13 -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/localSyntaxValidator.d.ts +9 -0
- package/dist/utils/localSyntaxValidator.js +103 -0
- package/dist/utils/systemPrompts.d.ts +1 -1
- package/dist/utils/systemPrompts.js +3 -6
- package/oclif.manifest.json +2 -2
- package/package.json +1 -2
- package/dist/utils/syntaxValidator.d.ts +0 -5
- package/dist/utils/syntaxValidator.js +0 -81
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,15 +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
|
-
const { accumulateUsage } = await import('./metrics.js');
|
|
216
|
-
accumulateUsage(result.usageMetadata);
|
|
217
260
|
const collector = getMetricCollector();
|
|
261
|
+
collector?.accumulateUsage(result.usageMetadata);
|
|
218
262
|
if (collector) {
|
|
219
263
|
collector.recordTokenUsage(result.usageMetadata.promptTokens || 0, result.usageMetadata.candidatesTokens || 0, result.usageMetadata.cachedTokens || 0);
|
|
220
264
|
}
|
|
@@ -323,8 +367,38 @@ export async function compressTextUsingFlashLite(text, instruction = "<directive
|
|
|
323
367
|
parts.push({ inlineData });
|
|
324
368
|
}
|
|
325
369
|
const contents = [{ role: 'user', parts }];
|
|
326
|
-
const
|
|
327
|
-
|
|
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
|
+
}
|
|
328
402
|
let textPart = '';
|
|
329
403
|
if (result.parts) {
|
|
330
404
|
// Use highly optimized local loop instead of array search to avoid callback allocation and naive database regex flags
|
|
@@ -340,6 +414,9 @@ export async function compressTextUsingFlashLite(text, instruction = "<directive
|
|
|
340
414
|
}
|
|
341
415
|
catch (error) {
|
|
342
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
|
+
}
|
|
343
420
|
return text; // fallback to raw text if compression fails
|
|
344
421
|
}
|
|
345
422
|
}
|
|
@@ -583,8 +660,38 @@ export async function generateChatTitle(firstMessage) {
|
|
|
583
660
|
let model = getGlobalActiveModel();
|
|
584
661
|
if (model === 'auto')
|
|
585
662
|
model = GEMINI_MODELS.FLASH_LITE;
|
|
586
|
-
const
|
|
587
|
-
|
|
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
|
+
}
|
|
588
695
|
let title = '';
|
|
589
696
|
if (result.parts) {
|
|
590
697
|
for (const p of result.parts) {
|
|
@@ -599,7 +706,15 @@ export async function generateChatTitle(firstMessage) {
|
|
|
599
706
|
return title ? title : firstMessage.substring(0, maxLength);
|
|
600
707
|
}
|
|
601
708
|
catch (error) {
|
|
602
|
-
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
|
+
}
|
|
603
718
|
return firstMessage.substring(0, maxLength);
|
|
604
719
|
}
|
|
605
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
|
/**
|
|
@@ -222,7 +222,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
222
222
|
cachedFiles.set(filePath, { text: readResult.output, inlineData: readResult.inlineData });
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
|
-
const MAX_TOTAL_FILES =
|
|
225
|
+
const MAX_TOTAL_FILES = 30;
|
|
226
226
|
try {
|
|
227
227
|
const { resolveAndValidateMultiWorkspacePath } = await import('../utils/pathSecurity.js');
|
|
228
228
|
const autoDiscovered = new Set();
|
|
@@ -255,10 +255,10 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
255
255
|
projectType,
|
|
256
256
|
summary: cacheHit.entry.summary,
|
|
257
257
|
relevantFiles: cachedFiles,
|
|
258
|
-
fromMemoryBank: true
|
|
258
|
+
fromMemoryBank: true,
|
|
259
259
|
},
|
|
260
260
|
targetAgent,
|
|
261
|
-
chainedMessages: []
|
|
261
|
+
chainedMessages: [],
|
|
262
262
|
};
|
|
263
263
|
}
|
|
264
264
|
// ─── Parallel Investigation Gate ─────────────────────────────
|
|
@@ -282,9 +282,6 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
282
282
|
const relevantFiles = new Map();
|
|
283
283
|
let summary = 'No relevant context found.';
|
|
284
284
|
let isInvestigationFinished = false;
|
|
285
|
-
const visitedToolCalls = new Set();
|
|
286
|
-
let consecutiveDuplicates = 0;
|
|
287
|
-
const MAX_CONSECUTIVE_DUPLICATES = 3;
|
|
288
285
|
let chainedMessages = [];
|
|
289
286
|
let webSearchSummary = '';
|
|
290
287
|
// Initial prompt
|
|
@@ -336,33 +333,6 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
336
333
|
if (abortSignal.aborted)
|
|
337
334
|
break;
|
|
338
335
|
const args = call.args;
|
|
339
|
-
// Deduplicate identical tool calls
|
|
340
|
-
const callSignature = `${call.name}:${JSON.stringify(args)}`;
|
|
341
|
-
if (call.name !== 'finish_investigation' && visitedToolCalls.has(callSignature)) {
|
|
342
|
-
consecutiveDuplicates++;
|
|
343
|
-
if (consecutiveDuplicates >= MAX_CONSECUTIVE_DUPLICATES) {
|
|
344
|
-
// Force-finish: the model is stuck in a loop, finalize with whatever context we have
|
|
345
|
-
const forceMsg = `Investigation auto-completed: the model repeated the same tool call ${MAX_CONSECUTIVE_DUPLICATES} times consecutively.`;
|
|
346
|
-
debugLog(`[Context Agent] ${forceMsg}`);
|
|
347
|
-
if (onProgress)
|
|
348
|
-
onProgress(forceMsg);
|
|
349
|
-
if (!summary || summary === 'No relevant context found.') {
|
|
350
|
-
summary = 'Investigation was auto-completed due to repeated duplicate tool calls. Review the gathered files for context.';
|
|
351
|
-
}
|
|
352
|
-
isFinished = true;
|
|
353
|
-
isInvestigationFinished = relevantFiles.size > 0;
|
|
354
|
-
break;
|
|
355
|
-
}
|
|
356
|
-
functionResponses.push({
|
|
357
|
-
functionResponse: {
|
|
358
|
-
name: call.name,
|
|
359
|
-
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.` }
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
consecutiveDuplicates = 0;
|
|
365
|
-
visitedToolCalls.add(callSignature);
|
|
366
336
|
let logMsg = ` [Context Agent] Executing ${call.name}`;
|
|
367
337
|
if (call.name === 'finish_investigation') {
|
|
368
338
|
const filesToRead = args.relevantFiles || [];
|
|
@@ -420,7 +390,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
420
390
|
// When the Context Agent finalizes its investigation, we automatically
|
|
421
391
|
// discover files that DEPEND ON the selected files. This ensures the
|
|
422
392
|
// Execution Agent won't break imports when modifying/deleting/renaming.
|
|
423
|
-
const MAX_TOTAL_FILES =
|
|
393
|
+
const MAX_TOTAL_FILES = 30;
|
|
424
394
|
try {
|
|
425
395
|
const { resolveAndValidateMultiWorkspacePath } = await import('../utils/pathSecurity.js');
|
|
426
396
|
const autoDiscovered = new Set();
|
|
@@ -599,7 +569,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
|
|
|
599
569
|
if (collector) {
|
|
600
570
|
collector.recordContextSelectedFiles(Array.from(relevantFiles.keys()));
|
|
601
571
|
if (!isInvestigationFinished || relevantFiles.size === 0) {
|
|
602
|
-
collector
|
|
572
|
+
collector?.recordInvestigationFailure?.();
|
|
603
573
|
}
|
|
604
574
|
}
|
|
605
575
|
if (isInvestigationFinished && relevantFiles.size > 0) {
|
|
@@ -16,15 +16,13 @@ export interface MetricCollector {
|
|
|
16
16
|
recordSelfCorrection(): void;
|
|
17
17
|
recordVerificationResult(passed: boolean): void;
|
|
18
18
|
recordMatchTier(tier: 'exact' | 'normalized' | 'levenshtein' | 'none'): void;
|
|
19
|
-
recordCacheHit(): void;
|
|
20
|
-
recordCacheMiss(): void;
|
|
21
|
-
recordInvestigationFailure(): void;
|
|
22
|
-
recordToolFailure(toolName: string): void;
|
|
23
|
-
recordModifyFailure(): void;
|
|
24
|
-
recordWriteFailure(): void;
|
|
25
|
-
recordCacheHit(cacheType: 'investigation' | 'read'): void;
|
|
26
|
-
recordCacheMiss(cacheType: 'investigation' | 'read'): void;
|
|
19
|
+
recordCacheHit(cacheType?: 'investigation' | 'read'): void;
|
|
20
|
+
recordCacheMiss(cacheType?: 'investigation' | 'read'): void;
|
|
27
21
|
recordCachePerformance(cacheType: 'investigation' | 'read', durationMs: number): void;
|
|
22
|
+
recordWriteFailure?(): void;
|
|
23
|
+
recordModifyFailure?(): void;
|
|
24
|
+
recordToolFailure?(toolName?: string): void;
|
|
25
|
+
recordInvestigationFailure?(): void;
|
|
28
26
|
}
|
|
29
27
|
export declare function setMetricCollector(collector: MetricCollector | null): void;
|
|
30
28
|
export declare function getMetricCollector(): MetricCollector | null;
|
|
@@ -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 || [];
|
|
@@ -27,7 +27,7 @@ import { buildDependencyGraph } from '../../utils/dependencyTracer.js';
|
|
|
27
27
|
import { debugLog } from '../../utils/logger.js';
|
|
28
28
|
// ─── Constants ───────────────────────────────────────────────────────
|
|
29
29
|
/** Maximum total relevant files across all agents after merge. */
|
|
30
|
-
const MAX_TOTAL_FILES =
|
|
30
|
+
const MAX_TOTAL_FILES = 30;
|
|
31
31
|
// ─── Investigation Orchestrator ──────────────────────────────────────
|
|
32
32
|
export class InvestigationOrchestrator {
|
|
33
33
|
/**
|
|
@@ -86,7 +86,7 @@ export declare class MessageBus {
|
|
|
86
86
|
private readonly persistPath;
|
|
87
87
|
private readonly workspaceRoot;
|
|
88
88
|
/** Maximum semantic signals any single agent can post */
|
|
89
|
-
static readonly MAX_SIGNALS_PER_AGENT =
|
|
89
|
+
static readonly MAX_SIGNALS_PER_AGENT = 50;
|
|
90
90
|
constructor(workspaceRoot: string, conversationId: string);
|
|
91
91
|
/**
|
|
92
92
|
* Records a tool execution into the activity log. Called by the scoped tool
|
|
@@ -38,7 +38,7 @@ export class MessageBus {
|
|
|
38
38
|
persistPath;
|
|
39
39
|
workspaceRoot;
|
|
40
40
|
/** Maximum semantic signals any single agent can post */
|
|
41
|
-
static MAX_SIGNALS_PER_AGENT =
|
|
41
|
+
static MAX_SIGNALS_PER_AGENT = 50;
|
|
42
42
|
constructor(workspaceRoot, conversationId) {
|
|
43
43
|
this.workspaceRoot = workspaceRoot;
|
|
44
44
|
const orchestrationDir = path.join(workspaceRoot, '.minovativemind', 'orchestration');
|
|
@@ -64,7 +64,7 @@ export class MessageBus {
|
|
|
64
64
|
* @returns `true` if the signal was accepted, `false` if the agent hit the cap.
|
|
65
65
|
*/
|
|
66
66
|
postSignal(signal) {
|
|
67
|
-
const agentSignalCount = this.signals.filter(s => s.fromAgent === signal.fromAgent).length;
|
|
67
|
+
const agentSignalCount = this.signals.filter((s) => s.fromAgent === signal.fromAgent).length;
|
|
68
68
|
if (agentSignalCount >= MessageBus.MAX_SIGNALS_PER_AGENT) {
|
|
69
69
|
debugLog(`MessageBus: Agent ${signal.fromAgent} hit signal cap (${MessageBus.MAX_SIGNALS_PER_AGENT}). ` +
|
|
70
70
|
`Dropping signal of type "${signal.type}".`);
|
|
@@ -85,12 +85,8 @@ export class MessageBus {
|
|
|
85
85
|
getUnread(agentId) {
|
|
86
86
|
const actCursor = this.activityCursors.get(agentId) ?? 0;
|
|
87
87
|
const sigCursor = this.signalCursors.get(agentId) ?? 0;
|
|
88
|
-
const activities = this.activityLog
|
|
89
|
-
|
|
90
|
-
.filter(e => e.agentId !== agentId);
|
|
91
|
-
const signals = this.signals
|
|
92
|
-
.slice(sigCursor)
|
|
93
|
-
.filter(s => s.fromAgent !== agentId);
|
|
88
|
+
const activities = this.activityLog.slice(actCursor).filter((e) => e.agentId !== agentId);
|
|
89
|
+
const signals = this.signals.slice(sigCursor).filter((s) => s.fromAgent !== agentId);
|
|
94
90
|
// Advance cursors to current end
|
|
95
91
|
this.activityCursors.set(agentId, this.activityLog.length);
|
|
96
92
|
this.signalCursors.set(agentId, this.signals.length);
|
|
@@ -111,7 +107,7 @@ export class MessageBus {
|
|
|
111
107
|
* recovery — collecting partial progress before re-dispatch).
|
|
112
108
|
*/
|
|
113
109
|
getAgentActivity(agentId) {
|
|
114
|
-
return this.activityLog.filter(e => e.agentId === agentId);
|
|
110
|
+
return this.activityLog.filter((e) => e.agentId === agentId);
|
|
115
111
|
}
|
|
116
112
|
/**
|
|
117
113
|
* Returns the total number of activity entries and signals in the bus.
|
|
@@ -131,11 +127,13 @@ export class MessageBus {
|
|
|
131
127
|
static formatActivityEntries(entries) {
|
|
132
128
|
if (entries.length === 0)
|
|
133
129
|
return '';
|
|
134
|
-
return entries
|
|
130
|
+
return entries
|
|
131
|
+
.map((e) => {
|
|
135
132
|
const statusIcon = e.status === 'success' ? '✓' : '✗';
|
|
136
133
|
const summary = e.resultSummary ? ` | ${e.resultSummary}` : '';
|
|
137
134
|
return ` ${e.agentId} | ${e.tool.padEnd(14)} → ${e.target.padEnd(40)} | ${statusIcon} ${e.action}${summary}`;
|
|
138
|
-
})
|
|
135
|
+
})
|
|
136
|
+
.join('\n');
|
|
139
137
|
}
|
|
140
138
|
/**
|
|
141
139
|
* Formats semantic signals into a readable string for agent context injection.
|
|
@@ -143,7 +141,8 @@ export class MessageBus {
|
|
|
143
141
|
static formatSignals(signals) {
|
|
144
142
|
if (signals.length === 0)
|
|
145
143
|
return '';
|
|
146
|
-
return signals
|
|
144
|
+
return signals
|
|
145
|
+
.map((s) => {
|
|
147
146
|
const tag = s.type.toUpperCase();
|
|
148
147
|
switch (s.type) {
|
|
149
148
|
case 'discovery':
|
|
@@ -157,7 +156,8 @@ export class MessageBus {
|
|
|
157
156
|
default:
|
|
158
157
|
return ` [SIGNAL from ${s.fromAgent}]: ${JSON.stringify(s)}`;
|
|
159
158
|
}
|
|
160
|
-
})
|
|
159
|
+
})
|
|
160
|
+
.join('\n');
|
|
161
161
|
}
|
|
162
162
|
// ─── Persistence ─────────────────────────────────────────────────
|
|
163
163
|
/**
|
|
@@ -173,7 +173,7 @@ export class MessageBus {
|
|
|
173
173
|
signalCursors: Object.fromEntries(this.signalCursors),
|
|
174
174
|
};
|
|
175
175
|
// Fire-and-forget — don't block the calling agent's tool loop
|
|
176
|
-
atomicWriteFile(this.persistPath, JSON.stringify(snapshot)).catch(err => {
|
|
176
|
+
atomicWriteFile(this.persistPath, JSON.stringify(snapshot)).catch((err) => {
|
|
177
177
|
debugLog(`MessageBus: Failed to persist to disk: ${err}`);
|
|
178
178
|
});
|
|
179
179
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* to prevent OOM on very large monorepos.
|
|
18
18
|
*/
|
|
19
19
|
import { debugLog } from '../../utils/logger.js';
|
|
20
|
+
import { getMetricCollector } from '../metrics.js';
|
|
20
21
|
// ─── Constants ───────────────────────────────────────────────────────
|
|
21
22
|
/** Maximum total bytes of cached file content before LRU eviction kicks in. */
|
|
22
23
|
const MAX_CACHE_BYTES = 5 * 1024 * 1024; // 5 MB
|
|
@@ -45,7 +46,6 @@ export class ReadCache {
|
|
|
45
46
|
*/
|
|
46
47
|
has(filePath) {
|
|
47
48
|
const found = this.cache.has(filePath);
|
|
48
|
-
const { getMetricCollector } = require('./metrics.js');
|
|
49
49
|
const collector = getMetricCollector();
|
|
50
50
|
if (found) {
|
|
51
51
|
this.hitCount++;
|
|
@@ -145,7 +145,8 @@ export async function executeScopedTool(name, args, workspaceRoot, agentId, bus,
|
|
|
145
145
|
else if (name === 'grep_search') {
|
|
146
146
|
targetDesc = args.pattern;
|
|
147
147
|
actionDesc = 'Searched';
|
|
148
|
-
const
|
|
148
|
+
const outputText = typeof result === 'object' && result?.output ? result.output : String(result || '');
|
|
149
|
+
const matchCount = (outputText.match(/\.\/[^:]+:\d+:/g) || []).length;
|
|
149
150
|
resultSummary = `${matchCount} matches`;
|
|
150
151
|
}
|
|
151
152
|
return result;
|
|
@@ -63,11 +63,14 @@ export class SubAgentRunner {
|
|
|
63
63
|
`${this.globalContext}\n` +
|
|
64
64
|
`</reference_context>\n\n` +
|
|
65
65
|
`<critical_guidelines>\n` +
|
|
66
|
-
`1. You are ONE worker in a team.
|
|
67
|
-
`2. DO NOT attempt to fulfill the entire original user request in the reference context. Other agents
|
|
68
|
-
`3.
|
|
69
|
-
`
|
|
70
|
-
`
|
|
66
|
+
`1. You are ONE worker in a team. Focus EXCLUSIVELY on your specific objective: "${this.intent}".\n` +
|
|
67
|
+
`2. DO NOT attempt to fulfill the entire original user request in the reference context. Other agents handle other tasks.\n` +
|
|
68
|
+
`3. EXECUTION WORKFLOW:\n` +
|
|
69
|
+
` - Step 1 (Locate): Call 'grep_search' or 'read_file' ONCE to inspect the file you need to edit.\n` +
|
|
70
|
+
` - Step 2 (Modify): Call 'modify_file' or 'write_file' to implement the required changes.\n` +
|
|
71
|
+
` - Step 3 (Conclude): Immediately STOP using tools and return a text summary of your changes.\n` +
|
|
72
|
+
`4. Do NOT call 'grep_search' or 'run_command' repeatedly in a loop. Once you have file context or command results, proceed directly to modifying code or responding with your text summary.\n` +
|
|
73
|
+
`5. Use 'post_message' only if you discover breaking changes affecting other agents.\n` +
|
|
71
74
|
`</critical_guidelines>`);
|
|
72
75
|
}
|
|
73
76
|
/**
|
|
@@ -87,12 +90,10 @@ export class SubAgentRunner {
|
|
|
87
90
|
*/
|
|
88
91
|
async execute(signal) {
|
|
89
92
|
return runWithAgentId(this.taskId, async () => {
|
|
90
|
-
debugLog(`SubAgent [${this.taskId}]: Starting execution.`);
|
|
91
|
-
this.lastHeartbeat = Date.now();
|
|
92
93
|
let success = false;
|
|
93
94
|
let finalSummary = '';
|
|
94
95
|
let crashed = false;
|
|
95
|
-
// Health
|
|
96
|
+
// Health monitor interval: checks every 5s if the last tool execution/heartbeat stalled
|
|
96
97
|
const healthMonitor = setInterval(() => {
|
|
97
98
|
if (Date.now() - this.lastHeartbeat > SubAgentRunner.STALL_TIMEOUT_MS) {
|
|
98
99
|
debugLog(`SubAgent [${this.taskId}]: STALL DETECTED. No heartbeat for ${SubAgentRunner.STALL_TIMEOUT_MS}ms.`);
|
|
@@ -103,14 +104,12 @@ export class SubAgentRunner {
|
|
|
103
104
|
}, 5000);
|
|
104
105
|
try {
|
|
105
106
|
// Send initial prompt
|
|
106
|
-
const prompt = `Begin execution for task: ${this.taskId}\nObjective: ${this.intent}\n\
|
|
107
|
+
const prompt = `Begin execution for task: ${this.taskId}\nObjective: ${this.intent}\n\nFollow the 3-step workflow: 1) inspect file -> 2) modify code -> 3) respond with text summary. Do not repeat search tools once results are returned.`;
|
|
107
108
|
let turnResult = await this.chat.sendMessage(prompt, undefined, signal);
|
|
108
109
|
this.updateUsage(turnResult);
|
|
109
110
|
// Tool Loop
|
|
110
111
|
const MAX_TURNS = Infinity;
|
|
111
112
|
let turns = 0;
|
|
112
|
-
// Anti-loop tracking
|
|
113
|
-
const visitedToolCalls = new Set();
|
|
114
113
|
while (turns < MAX_TURNS && !crashed && !signal.aborted) {
|
|
115
114
|
this.pingHeartbeat();
|
|
116
115
|
const calls = turnResult.response.functionCalls();
|
|
@@ -125,21 +124,6 @@ export class SubAgentRunner {
|
|
|
125
124
|
for (const call of calls) {
|
|
126
125
|
if (crashed || signal.aborted)
|
|
127
126
|
break;
|
|
128
|
-
// Anti-loop check: Hash the call to detect exact repetitions
|
|
129
|
-
const callSignature = `${call.name}:${JSON.stringify(call.args)}`;
|
|
130
|
-
if (visitedToolCalls.has(callSignature)) {
|
|
131
|
-
debugLog(`SubAgent [${this.taskId}]: Detected identical tool call ${call.name}. Blocking to prevent loop.`);
|
|
132
|
-
toolResponses.push({
|
|
133
|
-
functionResponse: {
|
|
134
|
-
name: call.name,
|
|
135
|
-
response: {
|
|
136
|
-
error: 'You have already made this exact tool call previously. Please review your context history or try a different action.',
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
visitedToolCalls.add(callSignature);
|
|
143
127
|
this.pingHeartbeat();
|
|
144
128
|
if (this.onProgress) {
|
|
145
129
|
this.onProgress(`executing ${call.name}...`);
|
|
@@ -152,6 +136,9 @@ export class SubAgentRunner {
|
|
|
152
136
|
catch (err) {
|
|
153
137
|
responseData = { error: err.message || String(err) };
|
|
154
138
|
}
|
|
139
|
+
// Log tool output to help diagnose issues (truncated to avoid giant logs)
|
|
140
|
+
const outputStr = JSON.stringify(responseData);
|
|
141
|
+
debugLog(`SubAgent [${this.taskId}]: Tool ${call.name} returned -> ${outputStr.length > 500 ? outputStr.substring(0, 1000) + '... (truncated)' : outputStr}`);
|
|
155
142
|
toolResponses.push({
|
|
156
143
|
functionResponse: {
|
|
157
144
|
name: call.name,
|
|
@@ -32,6 +32,14 @@ export declare function peekTurnUsage(): {
|
|
|
32
32
|
remainingBalance: number | undefined;
|
|
33
33
|
modelsUsed: Record<string, number>;
|
|
34
34
|
};
|
|
35
|
+
export declare function accumulateTurnUsage(usage: {
|
|
36
|
+
promptTokens?: number;
|
|
37
|
+
candidatesTokens?: number;
|
|
38
|
+
cachedTokens?: number;
|
|
39
|
+
promptTokenCount?: number;
|
|
40
|
+
candidatesTokenCount?: number;
|
|
41
|
+
cachedContentTokenCount?: number;
|
|
42
|
+
}, modelName: string): void;
|
|
35
43
|
/**
|
|
36
44
|
* Client service interacting directly with the serverless Gemini proxy endpoint.
|
|
37
45
|
* Ensures authorization via Firebase token passing and parses streamed content.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { debugLog } from '../utils/logger.js';
|
|
2
|
+
import { getMetricCollector } from './metrics.js';
|
|
2
3
|
/**
|
|
3
4
|
* ============================================================================
|
|
4
5
|
* PROXY CLIENT SERVICE
|
|
@@ -58,6 +59,16 @@ export function getAndResetTurnUsage() {
|
|
|
58
59
|
export function peekTurnUsage() {
|
|
59
60
|
return { ...globalSessionAccumulatedUsage };
|
|
60
61
|
}
|
|
62
|
+
export function accumulateTurnUsage(usage, modelName) {
|
|
63
|
+
const pTokens = usage.promptTokens ?? usage.promptTokenCount ?? 0;
|
|
64
|
+
const cTokens = usage.candidatesTokens ?? usage.candidatesTokenCount ?? 0;
|
|
65
|
+
const cachedTokens = usage.cachedTokens ?? usage.cachedContentTokenCount ?? 0;
|
|
66
|
+
globalSessionAccumulatedUsage.promptTokens += pTokens;
|
|
67
|
+
globalSessionAccumulatedUsage.candidatesTokens += cTokens;
|
|
68
|
+
globalSessionAccumulatedUsage.cachedTokens += cachedTokens;
|
|
69
|
+
globalSessionAccumulatedUsage.totalTokenCount += pTokens + cachedTokens + cTokens;
|
|
70
|
+
globalSessionAccumulatedUsage.modelsUsed[modelName] = (globalSessionAccumulatedUsage.modelsUsed[modelName] || 0) + 1;
|
|
71
|
+
}
|
|
61
72
|
/**
|
|
62
73
|
* Client service interacting directly with the serverless Gemini proxy endpoint.
|
|
63
74
|
* Ensures authorization via Firebase token passing and parses streamed content.
|
|
@@ -183,6 +194,12 @@ export class ProxyClient {
|
|
|
183
194
|
else if (data.type === 'done') {
|
|
184
195
|
if (data.usage) {
|
|
185
196
|
usageMetadata = data.usage;
|
|
197
|
+
const collector = getMetricCollector();
|
|
198
|
+
collector?.accumulateUsage({
|
|
199
|
+
promptTokens: data.usage.promptTokens || 0,
|
|
200
|
+
candidatesTokens: data.usage.candidatesTokens || 0,
|
|
201
|
+
cachedTokens: data.usage.cachedTokens || 0
|
|
202
|
+
});
|
|
186
203
|
globalSessionAccumulatedUsage.promptTokens += data.usage.promptTokens || 0;
|
|
187
204
|
globalSessionAccumulatedUsage.candidatesTokens += data.usage.candidatesTokens || 0;
|
|
188
205
|
globalSessionAccumulatedUsage.cachedTokens += data.usage.cachedTokens || 0;
|