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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { promises as fs, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { readCache, writeCache } from '../../utils/projectStorage.js';
|
|
5
|
+
import { debugLog } from '../../utils/logger.js';
|
|
6
|
+
const CACHE_FILE = 'investigation_cache.json';
|
|
7
|
+
const MAX_CACHE_SIZE_BYTES = 5 * 1024 * 1024; // 5MB
|
|
8
|
+
export function normalizePrompt(prompt) {
|
|
9
|
+
let normalized = prompt.toLowerCase().trim();
|
|
10
|
+
normalized = normalized.replace(/\s+/g, ' ');
|
|
11
|
+
const fillerWords = ['please ', 'can you ', 'could you ', 'help me ', 'i need '];
|
|
12
|
+
for (const word of fillerWords) {
|
|
13
|
+
if (normalized.startsWith(word)) {
|
|
14
|
+
normalized = normalized.slice(word.length).trim();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return normalized;
|
|
18
|
+
}
|
|
19
|
+
export function hashPrompt(normalized) {
|
|
20
|
+
return crypto.createHash('sha256').update(normalized).digest('hex');
|
|
21
|
+
}
|
|
22
|
+
export async function generateWorkspaceFingerprint(workspaceRoot, relevantFiles) {
|
|
23
|
+
const fileStats = [];
|
|
24
|
+
for (const file of relevantFiles) {
|
|
25
|
+
try {
|
|
26
|
+
const fullPath = path.join(workspaceRoot, file);
|
|
27
|
+
const stat = await fs.stat(fullPath);
|
|
28
|
+
// Use mtimeMs and size for a robust fingerprint
|
|
29
|
+
fileStats.push(`${file}:${stat.mtimeMs}:${stat.size}`);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
if (e.code === 'ENOENT') {
|
|
33
|
+
fileStats.push(`${file}:deleted`);
|
|
34
|
+
}
|
|
35
|
+
else if (e.code === 'EACCES' || e.code === 'EPERM') {
|
|
36
|
+
// Handle permission issues by marking as changed to force re-evaluation
|
|
37
|
+
fileStats.push(`${file}:inaccessible`);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// For other errors, assume it's changed
|
|
41
|
+
fileStats.push(`${file}:error`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
fileStats.sort();
|
|
46
|
+
return crypto.createHash('sha256').update(fileStats.join('\n')).digest('hex');
|
|
47
|
+
}
|
|
48
|
+
export async function lookupInvestigation(workspaceRoot, userPrompt) {
|
|
49
|
+
const normalized = normalizePrompt(userPrompt);
|
|
50
|
+
const hash = hashPrompt(normalized);
|
|
51
|
+
const store = readCache(workspaceRoot, CACHE_FILE);
|
|
52
|
+
if (!store || !store.entries || !store.entries[hash]) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const entry = store.entries[hash];
|
|
56
|
+
const currentFingerprint = await generateWorkspaceFingerprint(workspaceRoot, entry.relevantFiles);
|
|
57
|
+
if (entry.workspaceFingerprint !== currentFingerprint) {
|
|
58
|
+
debugLog(`Investigation cache: relevant files modified, invalidating cache entry`);
|
|
59
|
+
delete store.entries[hash];
|
|
60
|
+
writeCache(workspaceRoot, CACHE_FILE, store);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
debugLog(`Investigation cache: HIT for hash ${hash}`);
|
|
64
|
+
const bytesSaved = JSON.stringify(entry).length;
|
|
65
|
+
return { entry, bytesSaved };
|
|
66
|
+
}
|
|
67
|
+
export async function saveInvestigation(workspaceRoot, userPrompt, relevantFiles, summary) {
|
|
68
|
+
const store = readCache(workspaceRoot, CACHE_FILE) || { entries: {} };
|
|
69
|
+
const normalized = normalizePrompt(userPrompt);
|
|
70
|
+
const hash = hashPrompt(normalized);
|
|
71
|
+
const fingerprint = await generateWorkspaceFingerprint(workspaceRoot, relevantFiles);
|
|
72
|
+
store.entries[hash] = {
|
|
73
|
+
promptHash: hash,
|
|
74
|
+
relevantFiles,
|
|
75
|
+
summary,
|
|
76
|
+
workspaceFingerprint: fingerprint,
|
|
77
|
+
createdAt: Date.now(),
|
|
78
|
+
};
|
|
79
|
+
// Enforce 5MB LRU limit
|
|
80
|
+
let storeJson = JSON.stringify(store);
|
|
81
|
+
while (Buffer.byteLength(storeJson, 'utf8') > MAX_CACHE_SIZE_BYTES) {
|
|
82
|
+
const keys = Object.keys(store.entries);
|
|
83
|
+
if (keys.length === 0)
|
|
84
|
+
break;
|
|
85
|
+
let oldestKey = keys[0];
|
|
86
|
+
let oldestTime = store.entries[oldestKey].createdAt;
|
|
87
|
+
for (let i = 1; i < keys.length; i++) {
|
|
88
|
+
if (store.entries[keys[i]].createdAt < oldestTime) {
|
|
89
|
+
oldestKey = keys[i];
|
|
90
|
+
oldestTime = store.entries[keys[i]].createdAt;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
delete store.entries[oldestKey];
|
|
94
|
+
storeJson = JSON.stringify(store);
|
|
95
|
+
}
|
|
96
|
+
writeCache(workspaceRoot, CACHE_FILE, store);
|
|
97
|
+
debugLog(`💾 Investigation cached for future reuse`);
|
|
98
|
+
}
|
|
99
|
+
export async function invalidateFilesFromInvestigationCache(workspaceRoot, changedFiles) {
|
|
100
|
+
const store = readCache(workspaceRoot, CACHE_FILE);
|
|
101
|
+
if (!store || !store.entries)
|
|
102
|
+
return;
|
|
103
|
+
let invalidated = 0;
|
|
104
|
+
for (const hash of Object.keys(store.entries)) {
|
|
105
|
+
const entry = store.entries[hash];
|
|
106
|
+
const dependsOnChange = entry.relevantFiles.some((f) => changedFiles.includes(f));
|
|
107
|
+
if (dependsOnChange) {
|
|
108
|
+
delete store.entries[hash];
|
|
109
|
+
invalidated++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (invalidated > 0) {
|
|
113
|
+
writeCache(workspaceRoot, CACHE_FILE, store);
|
|
114
|
+
debugLog(`[DEBUG] Investigation cache: invalidated ${invalidated} entries referencing changed files`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function getInvestigationCacheStats(workspaceRoot) {
|
|
118
|
+
const store = readCache(workspaceRoot, CACHE_FILE);
|
|
119
|
+
const entriesCount = store?.entries ? Object.keys(store.entries).length : 0;
|
|
120
|
+
let sizeBytes = 0;
|
|
121
|
+
try {
|
|
122
|
+
const cachePath = path.join(workspaceRoot, '.minovativemind', CACHE_FILE);
|
|
123
|
+
const stat = statSync(cachePath);
|
|
124
|
+
sizeBytes = stat.size;
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
// ignore
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
entries: entriesCount,
|
|
131
|
+
sizeBytes,
|
|
132
|
+
hitCount: 0,
|
|
133
|
+
missCount: 0,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -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
|
/**
|
|
@@ -106,6 +106,10 @@ export class InvestigationOrchestrator {
|
|
|
106
106
|
`Files: ${mergedResult.relevantFiles.size} (deduped from ${totalFilesBeforeDedup}) | ` +
|
|
107
107
|
`Cache hits: ${cacheStats.hitCount}\n` +
|
|
108
108
|
` Duration: ${duration}s | Tokens: ${totalTokens.toLocaleString()} ${pc.dim(`(Input: ${totalInputTokens.toLocaleString()}, Output: ${totalOutputTokens.toLocaleString()})`)}`);
|
|
109
|
+
if (mergedResult && mergedResult.relevantFiles.size > 0) {
|
|
110
|
+
const { saveInvestigation } = await import('./investigationCache.js');
|
|
111
|
+
await saveInvestigation(workspaceRoot, userRequest, Array.from(mergedResult.relevantFiles.keys()), mergedResult.summary);
|
|
112
|
+
}
|
|
109
113
|
return mergedResult;
|
|
110
114
|
}
|
|
111
115
|
/**
|
|
@@ -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
|
}
|
|
@@ -260,7 +260,7 @@ export class Orchestrator {
|
|
|
260
260
|
'DO NOT list changes by task name or separate them by agent. ' +
|
|
261
261
|
'Be concise, helpful, and conclude by asking if they need any further adjustments.\n' +
|
|
262
262
|
'</directives>';
|
|
263
|
-
const synthesized = await compressTextUsingFlashLite(payloadWithContext, instruction);
|
|
263
|
+
const synthesized = await compressTextUsingFlashLite(payloadWithContext, instruction, undefined, true);
|
|
264
264
|
finalSummary += synthesized + '\n\n';
|
|
265
265
|
}
|
|
266
266
|
catch (e) {
|
|
@@ -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,11 +46,14 @@ export class ReadCache {
|
|
|
45
46
|
*/
|
|
46
47
|
has(filePath) {
|
|
47
48
|
const found = this.cache.has(filePath);
|
|
49
|
+
const collector = getMetricCollector();
|
|
48
50
|
if (found) {
|
|
49
51
|
this.hitCount++;
|
|
52
|
+
collector?.recordCacheHit();
|
|
50
53
|
}
|
|
51
54
|
else {
|
|
52
55
|
this.missCount++;
|
|
56
|
+
collector?.recordCacheMiss();
|
|
53
57
|
}
|
|
54
58
|
return found;
|
|
55
59
|
}
|
|
@@ -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}...`);
|
|
@@ -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;
|
|
@@ -11,7 +11,7 @@ export async function detectVerificationCommand(workspaceRoot) {
|
|
|
11
11
|
{ name: 'pnpm-lock.yaml', prefix: 'pnpm run' },
|
|
12
12
|
{ name: 'yarn.lock', prefix: 'yarn run' },
|
|
13
13
|
{ name: 'bun.lockb', prefix: 'bun run' },
|
|
14
|
-
{ name: 'bun.lock', prefix: 'bun run' }
|
|
14
|
+
{ name: 'bun.lock', prefix: 'bun run' },
|
|
15
15
|
];
|
|
16
16
|
for (const lf of lockFiles) {
|
|
17
17
|
try {
|
|
@@ -66,14 +66,20 @@ export async function detectVerificationCommand(workspaceRoot) {
|
|
|
66
66
|
catch { }
|
|
67
67
|
try {
|
|
68
68
|
await fs.access(path.join(workspaceRoot, 'build.gradle'));
|
|
69
|
-
return (await fs
|
|
69
|
+
return (await fs
|
|
70
|
+
.access(path.join(workspaceRoot, 'gradlew'))
|
|
71
|
+
.then(() => true)
|
|
72
|
+
.catch(() => false))
|
|
70
73
|
? './gradlew classes testClasses'
|
|
71
74
|
: 'gradle classes testClasses';
|
|
72
75
|
}
|
|
73
76
|
catch { }
|
|
74
77
|
try {
|
|
75
78
|
await fs.access(path.join(workspaceRoot, 'build.gradle.kts'));
|
|
76
|
-
return (await fs
|
|
79
|
+
return (await fs
|
|
80
|
+
.access(path.join(workspaceRoot, 'gradlew'))
|
|
81
|
+
.then(() => true)
|
|
82
|
+
.catch(() => false))
|
|
77
83
|
? './gradlew classes testClasses'
|
|
78
84
|
: 'gradle classes testClasses';
|
|
79
85
|
}
|
|
@@ -126,7 +132,10 @@ export async function detectVerificationCommand(workspaceRoot) {
|
|
|
126
132
|
}
|
|
127
133
|
try {
|
|
128
134
|
await fs.access(path.join(workspaceRoot, 'Gemfile'));
|
|
129
|
-
return (await fs
|
|
135
|
+
return (await fs
|
|
136
|
+
.access(path.join(workspaceRoot, 'spec'))
|
|
137
|
+
.then(() => true)
|
|
138
|
+
.catch(() => false))
|
|
130
139
|
? 'bundle exec rspec'
|
|
131
140
|
: 'bundle exec rubocop';
|
|
132
141
|
}
|
|
@@ -137,7 +146,7 @@ export async function runVerification(workspaceRoot, abortSignal) {
|
|
|
137
146
|
const command = await detectVerificationCommand(workspaceRoot);
|
|
138
147
|
if (!command)
|
|
139
148
|
return null;
|
|
140
|
-
const MAX_VERIFY_OUTPUT =
|
|
149
|
+
const MAX_VERIFY_OUTPUT = 50_000; // 50KB cap on verification output
|
|
141
150
|
debugLog(`Running project-level verification command: ${command}`);
|
|
142
151
|
try {
|
|
143
152
|
const { stdout, stderr } = await execAsync(command, {
|
|
@@ -225,7 +234,7 @@ ${result.errors.join('\n')}
|
|
|
225
234
|
|
|
226
235
|
Please fix these errors using the modify_file tool.`;
|
|
227
236
|
}
|
|
228
|
-
import { auditFilePerformance, formatAuditForModel, formatAuditForTerminal, isAuditableFile } from '../utils/performanceAuditor.js';
|
|
237
|
+
import { auditFilePerformance, formatAuditForModel, formatAuditForTerminal, isAuditableFile, } from '../utils/performanceAuditor.js';
|
|
229
238
|
export async function verifyChangedFiles(workspaceRoot, filePaths, abortSignal) {
|
|
230
239
|
const errors = [];
|
|
231
240
|
const perfAudits = [];
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -23,3 +23,7 @@ export declare const GEMINI_MODELS: {
|
|
|
23
23
|
export declare const DEFAULT_MODEL: "auto";
|
|
24
24
|
/** Maximum tokens the model can output per response. */
|
|
25
25
|
export declare const MAX_OUTPUT_TOKENS = 60000;
|
|
26
|
+
/**
|
|
27
|
+
* Checks if BYOK is currently enabled for the user.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isByokEnabled(): Promise<boolean>;
|
package/dist/utils/config.js
CHANGED
|
@@ -23,3 +23,11 @@ export const GEMINI_MODELS = {
|
|
|
23
23
|
export const DEFAULT_MODEL = GEMINI_MODELS.AUTO;
|
|
24
24
|
/** Maximum tokens the model can output per response. */
|
|
25
25
|
export const MAX_OUTPUT_TOKENS = 60_000;
|
|
26
|
+
/**
|
|
27
|
+
* Checks if BYOK is currently enabled for the user.
|
|
28
|
+
*/
|
|
29
|
+
export async function isByokEnabled() {
|
|
30
|
+
const { loadCredentials } = await import('./credentialStore.js');
|
|
31
|
+
const creds = await loadCredentials();
|
|
32
|
+
return !!(creds.useByok && creds.geminiApiKey);
|
|
33
|
+
}
|
|
@@ -4,6 +4,8 @@ export interface StoredCredentials {
|
|
|
4
4
|
idToken?: string;
|
|
5
5
|
refreshToken?: string;
|
|
6
6
|
idTokenExpiry?: number;
|
|
7
|
+
geminiApiKey?: string;
|
|
8
|
+
useByok?: boolean;
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
9
11
|
* Persists authentication credentials to the most secure available store.
|
|
@@ -15,6 +17,11 @@ export interface StoredCredentials {
|
|
|
15
17
|
* 4. AES-256-GCM encrypted file with 0600 permissions
|
|
16
18
|
*/
|
|
17
19
|
export declare function saveCredentials(data: StoredCredentials): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Updates a specific credential field without reloading all existing fields.
|
|
22
|
+
* Useful for partial updates (e.g., toggling BYOK).
|
|
23
|
+
*/
|
|
24
|
+
export declare function updateCredentialField<K extends keyof StoredCredentials>(key: K, value: StoredCredentials[K]): Promise<void>;
|
|
18
25
|
/**
|
|
19
26
|
* Loads authentication credentials from the secure store.
|
|
20
27
|
* Returns an empty object if no credentials are found.
|
|
@@ -388,6 +388,14 @@ export async function saveCredentials(data) {
|
|
|
388
388
|
}
|
|
389
389
|
}
|
|
390
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* Updates a specific credential field without reloading all existing fields.
|
|
393
|
+
* Useful for partial updates (e.g., toggling BYOK).
|
|
394
|
+
*/
|
|
395
|
+
export async function updateCredentialField(key, value) {
|
|
396
|
+
const current = await loadCredentials();
|
|
397
|
+
await saveCredentials({ ...current, [key]: value });
|
|
398
|
+
}
|
|
391
399
|
/**
|
|
392
400
|
* Loads authentication credentials from the secure store.
|
|
393
401
|
* Returns an empty object if no credentials are found.
|
|
@@ -222,6 +222,13 @@ export async function invalidateCacheForDependents(workspaceRoot, changedFiles)
|
|
|
222
222
|
if (updated) {
|
|
223
223
|
writeCache(workspaceRoot, 'context_cache.json', cachedContext);
|
|
224
224
|
}
|
|
225
|
+
try {
|
|
226
|
+
const { invalidateFilesFromInvestigationCache } = await import('../services/orchestration/investigationCache.js');
|
|
227
|
+
await invalidateFilesFromInvestigationCache(workspaceRoot, changedFiles);
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
// Ignored
|
|
231
|
+
}
|
|
225
232
|
}
|
|
226
233
|
catch (err) {
|
|
227
234
|
console.warn(pc.yellow(`Failed to invalidate cache for dependents: ${err}`));
|