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.
Files changed (35) hide show
  1. package/README.md +5 -0
  2. package/dist/commands/chat.js +1 -0
  3. package/dist/services/agent/slashCommands.js +106 -2
  4. package/dist/services/agent/syntaxAgent.d.ts +10 -0
  5. package/dist/services/agent/syntaxAgent.js +52 -0
  6. package/dist/services/agent/toolLoop.js +5 -17
  7. package/dist/services/agent-tools.js +89 -28
  8. package/dist/services/agent.js +1 -0
  9. package/dist/services/ai.d.ts +1 -1
  10. package/dist/services/ai.js +128 -13
  11. package/dist/services/changeLogger.js +2 -2
  12. package/dist/services/contextAgent.js +5 -35
  13. package/dist/services/metrics.d.ts +6 -8
  14. package/dist/services/orchestration/investigationAgent.js +0 -32
  15. package/dist/services/orchestration/investigationOrchestrator.js +1 -1
  16. package/dist/services/orchestration/messageBus.d.ts +1 -1
  17. package/dist/services/orchestration/messageBus.js +14 -14
  18. package/dist/services/orchestration/readCache.js +1 -1
  19. package/dist/services/orchestration/scopedTools.js +2 -1
  20. package/dist/services/orchestration/subAgent.js +13 -26
  21. package/dist/services/proxyClient.d.ts +8 -0
  22. package/dist/services/proxyClient.js +17 -0
  23. package/dist/services/verificationService.js +15 -6
  24. package/dist/utils/config.d.ts +4 -0
  25. package/dist/utils/config.js +8 -0
  26. package/dist/utils/credentialStore.d.ts +7 -0
  27. package/dist/utils/credentialStore.js +8 -0
  28. package/dist/utils/localSyntaxValidator.d.ts +9 -0
  29. package/dist/utils/localSyntaxValidator.js +103 -0
  30. package/dist/utils/systemPrompts.d.ts +1 -1
  31. package/dist/utils/systemPrompts.js +3 -6
  32. package/oclif.manifest.json +2 -2
  33. package/package.json +1 -2
  34. package/dist/utils/syntaxValidator.d.ts +0 -5
  35. package/dist/utils/syntaxValidator.js +0 -81
@@ -1,12 +1,14 @@
1
- import { GEMINI_MODELS, DEFAULT_MODEL, MAX_OUTPUT_TOKENS } from '../utils/config.js';
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 { ProxyClient } from './proxyClient.js';
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 result = await proxyClient.generateFunctionCallViaProxy(idToken, this.modelName, this.history, this.tools, undefined, // toolConfig
210
- this.systemInstruction, effectiveGenerationConfig, onChunk ? { onChunk } : undefined, // streamCallbacks
211
- abortSignal);
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 result = await proxyClient.generateFunctionCallViaProxy(idToken, model, contents, [], // no tools
327
- undefined, instruction, { temperature: 0.2 });
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 result = await proxyClient.generateFunctionCallViaProxy(idToken, model, contents, [], // no tools
587
- undefined, instruction, { temperature: 0.2 });
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 = 10;
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 = 15;
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 = 15;
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.recordInvestigationFailure();
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 = 15;
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 = 20;
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 = 20;
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
- .slice(actCursor)
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.map(e => {
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
- }).join('\n');
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.map(s => {
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
- }).join('\n');
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 matchCount = Array.isArray(result) ? result.length : 0;
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. You MUST ONLY focus 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 are handling the other parts.\n` +
68
- `3. You are part of a parallelized system. Use 'post_message' to coordinate if you discover breaking changes.\n` +
69
- `4. When you have completed your objective, stop using tools and provide a final summary of your work.\n` +
70
- `5. If you encounter an insurmountable error, provide a summary of what went wrong so the orchestrator can re-assign or fix it.\n` +
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 Monitor Timer
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\nYou must use tools to achieve this objective. Do not stop until the objective is fully complete.`;
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;