minovative-mind-cli 2.13.3 → 2.13.4

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.
@@ -1,4 +1,5 @@
1
1
  import { type Content, type FunctionCall } from '@google/generative-ai';
2
+ export declare function getMultiWorkspaceBlock(): string;
2
3
  export declare function setModelOverride(agent: 'context' | 'execution', overrideStr: string): void;
3
4
  export declare function clearModelOverrides(): void;
4
5
  export declare function setGlobalActiveModel(model: string): void;
@@ -9,11 +10,14 @@ export declare class ProxyChatSession {
9
10
  private modelName;
10
11
  private systemInstruction;
11
12
  private tools;
13
+ private toolConfig?;
12
14
  private generationConfig;
13
15
  private latestUsageMetadata;
14
- constructor(modelName: string, systemInstruction: string, tools: any[], generationConfig: any);
16
+ constructor(modelName: string, systemInstruction: string, tools: any[], generationConfig: any, toolConfig?: any);
15
17
  getLatestUsageMetadata(): any;
16
- setAgentConfig(systemInstruction: string, tools: any[]): void;
18
+ setAgentConfig(systemInstruction: string, tools: any[], toolConfig?: any): void;
19
+ setToolConfig(toolConfig?: any): void;
20
+ getToolConfig(): any;
17
21
  setModel(modelName: string): void;
18
22
  getModel(): string;
19
23
  clearHistory(): void;
@@ -9,7 +9,7 @@ import { GENERAL_CHAT_INSTRUCTION, PLAN_EXECUTION_INSTRUCTION, PLAN_MODE_INSTRUC
9
9
  import { workspaceRegistry } from './workspaceRegistry.js';
10
10
  import { loadCredentials } from '../utils/credentialStore.js';
11
11
  import { ProxyClient } from './proxyClient.js';
12
- function getMultiWorkspaceBlock() {
12
+ export function getMultiWorkspaceBlock() {
13
13
  const summary = workspaceRegistry.buildPromptSummary();
14
14
  const primaryRoot = process.cwd();
15
15
  const primaryName = primaryRoot.split(/[/\\]/).pop() || 'Primary';
@@ -99,20 +99,29 @@ export class ProxyChatSession {
99
99
  modelName;
100
100
  systemInstruction;
101
101
  tools;
102
+ toolConfig;
102
103
  generationConfig;
103
104
  latestUsageMetadata = undefined;
104
- constructor(modelName, systemInstruction, tools, generationConfig) {
105
+ constructor(modelName, systemInstruction, tools, generationConfig, toolConfig) {
105
106
  this.modelName = modelName;
106
107
  this.systemInstruction = systemInstruction;
107
108
  this.tools = tools;
108
109
  this.generationConfig = generationConfig;
110
+ this.toolConfig = toolConfig;
109
111
  }
110
112
  getLatestUsageMetadata() {
111
113
  return this.latestUsageMetadata;
112
114
  }
113
- setAgentConfig(systemInstruction, tools) {
115
+ setAgentConfig(systemInstruction, tools, toolConfig) {
114
116
  this.systemInstruction = systemInstruction;
115
117
  this.tools = tools;
118
+ this.toolConfig = toolConfig;
119
+ }
120
+ setToolConfig(toolConfig) {
121
+ this.toolConfig = toolConfig;
122
+ }
123
+ getToolConfig() {
124
+ return this.toolConfig;
116
125
  }
117
126
  setModel(modelName) {
118
127
  this.modelName = modelName;
@@ -374,13 +383,11 @@ export class ProxyChatSession {
374
383
  let result;
375
384
  if (byokEnabled) {
376
385
  const creds = await loadCredentials();
377
- result = await proxyClient.generateViaBYOK(creds.geminiApiKey, this.modelName, this.history, this.tools, undefined, // toolConfig
378
- this.systemInstruction, effectiveGenerationConfig, onChunk ? { onChunk } : undefined, // streamCallbacks
386
+ result = await proxyClient.generateViaBYOK(creds.geminiApiKey, this.modelName, this.history, this.tools, this.toolConfig, this.systemInstruction, effectiveGenerationConfig, onChunk ? { onChunk } : undefined, // streamCallbacks
379
387
  actualAbortSignal);
380
388
  }
381
389
  else {
382
- result = await proxyClient.generateFunctionCallViaProxy(idToken, this.modelName, this.history, this.tools, undefined, // toolConfig
383
- this.systemInstruction, effectiveGenerationConfig, onChunk ? { onChunk } : undefined, // streamCallbacks
390
+ result = await proxyClient.generateFunctionCallViaProxy(idToken, this.modelName, this.history, this.tools, this.toolConfig, this.systemInstruction, effectiveGenerationConfig, onChunk ? { onChunk } : undefined, // streamCallbacks
384
391
  actualAbortSignal);
385
392
  }
386
393
  this.latestUsageMetadata = result.usageMetadata;
@@ -697,6 +704,10 @@ export function createContextAgentSession() {
697
704
  temperature: contextTempOverride !== null ? contextTempOverride : 1,
698
705
  topP: 0.95,
699
706
  topK: 40,
707
+ }, {
708
+ functionCallingConfig: {
709
+ mode: 'ANY',
710
+ },
700
711
  });
701
712
  }
702
713
  // ─── Intent Router Service ───────────────────────────────────────────
@@ -2,9 +2,7 @@ import { promises as fs } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import pc from 'picocolors';
4
4
  import { createContextAgentSession, createIntentRouterSession, createWebSearchAgentSession, createExecutionComplexitySession, } from './ai.js';
5
- import { evaluateInvestigationComplexity } from './investigationComplexity.js';
6
- import { InvestigationOrchestrator } from './orchestration/investigationOrchestrator.js';
7
- import { isSubAgentsEnabled, executeTool } from './agent-tools.js';
5
+ import { executeTool } from './agent-tools.js';
8
6
  import { debugLog } from '../utils/logger.js';
9
7
  import { buildDependencyGraph } from '../utils/dependencyTracer.js';
10
8
  import { runEphemeralScript } from '../utils/analysisRunner.js';
@@ -418,23 +416,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
418
416
  chainedMessages: [],
419
417
  };
420
418
  }
421
- // ─── Parallel Investigation Gate ─────────────────────────────
422
- if (isSubAgentsEnabled()) {
423
- // Determine complexity and domain breakdown
424
- const approxFiles = projectTree.split('\n').length;
425
- const complexity = await evaluateInvestigationComplexity(userRequest, projectType, approxFiles, chatHistory, abortSignal);
426
- if (complexity.strategy === 'PARALLEL' && complexity.agentAssignments.length > 0) {
427
- const orchestrator = new InvestigationOrchestrator();
428
- const parallelResult = await orchestrator.runParallelInvestigation(userRequest, complexity.agentAssignments, workspaceRoot, projectTree, projectType, chatHistory, abortSignal, onProgress, onToolCall);
429
- if (parallelResult !== null) {
430
- // If parallel investigation succeeded, we are done. Return immediately.
431
- parallelResult.isParallel = true;
432
- return { contextResult: parallelResult, targetAgent, chainedMessages: [] };
433
- }
434
- // If it returned null, all agents crashed. Fall through to single agent fallback.
435
- }
436
- }
437
- // ─── Single Agent Path (Fallback/Default) ────────────────────
419
+ // ─── Single Context Agent (Unified Reconnaissance) ───────────
438
420
  const session = createContextAgentSession();
439
421
  const relevantFiles = new Map();
440
422
  let summary = 'No relevant context found.';
@@ -450,6 +432,8 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
450
432
  }
451
433
  currentMessage += `\n\nStart investigating to find relevant files.`;
452
434
  const MAX_TURNS = Infinity;
435
+ let textRetryCount = 0;
436
+ const MAX_TEXT_RETRIES = 2;
453
437
  for (let turn = 0; turn < MAX_TURNS; turn++) {
454
438
  if (abortSignal.aborted) {
455
439
  const err = new Error('Operation aborted');
@@ -489,8 +473,34 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
489
473
  }
490
474
  const functionCalls = result.response.functionCalls();
491
475
  if (!functionCalls || functionCalls.length === 0) {
492
- // Model returned text instead of calling finish_investigation, just take the text as summary
493
- summary = result.response.text();
476
+ const rawText = result.response.text() || '';
477
+ if (textRetryCount < MAX_TEXT_RETRIES) {
478
+ textRetryCount++;
479
+ debugLog(`[Context Agent] Model returned text with 0 tool calls. Rejecting and enforcing tool call (attempt ${textRetryCount}/${MAX_TEXT_RETRIES}).`);
480
+ currentMessage = `[SYSTEM DIRECTIVE (CRITICAL)]: You are a background investigation agent and must NEVER output conversational text directly. You MUST call tools (such as 'search_codebase', 'read_file', 'list_directory', 'find_dependencies', or 'finish_investigation'). You MUST conclude by calling the 'finish_investigation' tool with your findings and relevantFiles array.`;
481
+ continue;
482
+ }
483
+ // Fallback after retries: extract file paths from text
484
+ summary = rawText;
485
+ const fileRegex = /(?:[a-zA-Z0-9_-]+\/)+[a-zA-Z0-9_.-]+\.[a-zA-Z0-9]+/g;
486
+ const matches = rawText.match(fileRegex) || [];
487
+ const potentialFiles = Array.from(new Set(matches));
488
+ if (potentialFiles.length > 0) {
489
+ debugLog(`[Context Agent] Recovered ${potentialFiles.length} potential files from fallback text regex: ${potentialFiles.join(', ')}`);
490
+ const readResults = await Promise.all(potentialFiles.map(async (filePath) => {
491
+ const readResult = await executeTool(workspaceRoot, 'read_file', { filePath });
492
+ return { filePath, readResult };
493
+ }));
494
+ for (const { filePath, readResult } of readResults) {
495
+ if (cumulativeFileTokens >= MAX_TOTAL_FILE_TOKENS)
496
+ break;
497
+ if (!readResult.error) {
498
+ const boundedText = boundFileContent(filePath, readResult.output);
499
+ relevantFiles.set(filePath, { text: boundedText, inlineData: readResult.inlineData });
500
+ cumulativeFileTokens += estimateTokenCount(boundedText);
501
+ }
502
+ }
503
+ }
494
504
  break;
495
505
  }
496
506
  let isFinished = false;
@@ -11,13 +11,14 @@
11
11
  * components" + "Auth UI styling") and produces an `InvestigationResult` containing
12
12
  * the files it found and a summary of its findings.
13
13
  */
14
- import { ProxyChatSession, getContextToolDeclarations, getGlobalActiveModel } from '../ai.js';
14
+ import { ProxyChatSession, getContextToolDeclarations, getGlobalActiveModel, getMultiWorkspaceBlock } from '../ai.js';
15
15
  import { GEMINI_MODELS, MAX_OUTPUT_TOKENS } from '../../utils/config.js';
16
16
  import { CONTEXT_SYSTEM_INSTRUCTION } from '../../utils/systemPrompts.js';
17
17
  import { debugLog } from '../../utils/logger.js';
18
18
  import { listDirectory, grepSearch, readFile, traceDependencies, findRecentChanges } from '../agent-tools.js';
19
19
  import { runEphemeralScript } from '../../utils/analysisRunner.js';
20
20
  import { formatToolCall } from '../agent/toolLoop.js';
21
+ import { pruneTextToTokenBudget } from '../../utils/historyPrompt.js';
21
22
  // ─── Investigation Agent Runner ──────────────────────────────────────
22
23
  /**
23
24
  * Executes a read-only investigation within a scoped set of domains.
@@ -52,12 +53,16 @@ export class InvestigationAgentRunner {
52
53
  this.projectType = projectType;
53
54
  let model = getGlobalActiveModel();
54
55
  if (model === GEMINI_MODELS.AUTO || model.includes('claude'))
55
- model = GEMINI_MODELS.FLASH_LITE;
56
+ model = GEMINI_MODELS.FLASH;
56
57
  this.chat = new ProxyChatSession(model, this.buildSystemInstruction(), getContextToolDeclarations(), {
57
58
  maxOutputTokens: MAX_OUTPUT_TOKENS,
58
59
  temperature: 1,
59
60
  topP: 0.95,
60
61
  topK: 40,
62
+ }, {
63
+ functionCallingConfig: {
64
+ mode: 'ANY',
65
+ },
61
66
  });
62
67
  }
63
68
  /**
@@ -66,7 +71,7 @@ export class InvestigationAgentRunner {
66
71
  */
67
72
  buildSystemInstruction() {
68
73
  const domainList = this.domains.map((d) => ` - ${d}`).join('\n');
69
- return (CONTEXT_SYSTEM_INSTRUCTION +
74
+ return (CONTEXT_SYSTEM_INSTRUCTION.replace('{{MULTI_WORKSPACE_BLOCK}}', getMultiWorkspaceBlock()) +
70
75
  `\n\n<investigation_scope>` +
71
76
  `\nYou are investigation agent "${this.agentLabel}", part of a parallel investigation team.` +
72
77
  `\nYour assigned investigation domains:\n${domainList}` +
@@ -111,10 +116,13 @@ export class InvestigationAgentRunner {
111
116
  // Build the initial investigation prompt
112
117
  let currentMessage = `User Request: "${userRequest}"\n\nProject Type: ${this.projectType}\n\nProject Structure:\n${this.projectTree}`;
113
118
  if (chatHistory) {
114
- currentMessage = `Previous Conversation Context:\n${chatHistory}\n\n` + currentMessage;
119
+ const prunedHistory = pruneTextToTokenBudget(chatHistory, 4500, { fromStart: true });
120
+ currentMessage = `Previous Conversation Context:\n${prunedHistory}\n\n` + currentMessage;
115
121
  }
116
122
  currentMessage += `\n\nStart investigating to find relevant files within your assigned domains.`;
117
123
  let isFinished = false;
124
+ let textRetryCount = 0;
125
+ const MAX_TEXT_RETRIES = 2;
118
126
  // Tool loop — mirrors the existing context agent loop in contextAgent.ts
119
127
  while (!crashed && !abortSignal.aborted && !isFinished) {
120
128
  this.pingHeartbeat();
@@ -130,8 +138,34 @@ export class InvestigationAgentRunner {
130
138
  this.updateUsage(result);
131
139
  const functionCalls = result.response.functionCalls();
132
140
  if (!functionCalls || functionCalls.length === 0) {
133
- // Model returned text take as summary
134
- summary = result.response.text();
141
+ const rawText = result.response.text() || '';
142
+ if (textRetryCount < MAX_TEXT_RETRIES) {
143
+ textRetryCount++;
144
+ debugLog(`InvestigationAgent [${this.agentLabel}]: Model returned text with 0 tool calls. Rejecting and enforcing tool call (attempt ${textRetryCount}/${MAX_TEXT_RETRIES}).`);
145
+ currentMessage = `[SYSTEM DIRECTIVE (CRITICAL)]: You are a background investigation agent and must NEVER output conversational text directly. You MUST call tools (such as 'search_codebase', 'read_file', 'list_directory', 'find_dependencies', or 'finish_investigation'). You MUST conclude by calling the 'finish_investigation' tool with your findings and relevantFiles array.`;
146
+ continue;
147
+ }
148
+ // Fallback after retries: attempt to extract file paths mentioned in the text using regex
149
+ summary = rawText;
150
+ const fileRegex = /(?:[a-zA-Z0-9_-]+\/)+[a-zA-Z0-9_.-]+\.[a-zA-Z0-9]+/g;
151
+ const matches = rawText.match(fileRegex) || [];
152
+ const potentialFiles = Array.from(new Set(matches));
153
+ if (potentialFiles.length > 0) {
154
+ debugLog(`InvestigationAgent [${this.agentLabel}]: Recovered ${potentialFiles.length} potential files from fallback text regex: ${potentialFiles.join(', ')}`);
155
+ await Promise.all(potentialFiles.map(async (filePath) => {
156
+ if (this.readCache.has(filePath)) {
157
+ relevantFiles.set(filePath, this.readCache.get(filePath));
158
+ }
159
+ else {
160
+ const readResult = await readFile(this.workspaceRoot, filePath);
161
+ if (!readResult.error) {
162
+ const contentObj = { text: readResult.output, inlineData: readResult.inlineData };
163
+ relevantFiles.set(filePath, contentObj);
164
+ this.readCache.set(filePath, contentObj);
165
+ }
166
+ }
167
+ }));
168
+ }
135
169
  success = true;
136
170
  break;
137
171
  }
@@ -17,15 +17,26 @@
17
17
  * to prevent OOM on very large monorepos.
18
18
  */
19
19
  /**
20
- * In-memory file content cache shared across parallel investigation agents.
20
+ * Computes a standardized cache key for full-file or ranged/targeted read requests.
21
+ *
22
+ * @param filePath - Target relative or aliased file path.
23
+ * @param startLine - Optional 1-indexed starting line.
24
+ * @param endLine - Optional 1-indexed ending line.
25
+ * @param targetElements - Optional array of AST symbol names.
26
+ * @returns Serialized cache key string.
27
+ */
28
+ export declare function computeReadCacheKey(filePath: string, startLine?: number, endLine?: number, targetElements?: string[]): string;
29
+ /**
30
+ * In-memory file content cache shared across parallel investigation agents
31
+ * or scoped to individual execution sub-agents.
21
32
  *
22
33
  * Usage:
23
34
  * ```ts
24
35
  * const cache = new ReadCache()
25
- * // In tool wrapper:
26
- * if (cache.has(filePath)) return cache.get(filePath)!
27
- * const content = await fs.readFile(absPath, 'utf-8')
28
- * cache.set(filePath, content)
36
+ * const key = computeReadCacheKey(filePath, startLine, endLine)
37
+ * if (cache.has(key)) return cache.get(key)!
38
+ * const content = await readFile(root, filePath, startLine, endLine)
39
+ * cache.set(key, content)
29
40
  * ```
30
41
  */
31
42
  export declare class ReadCache {
@@ -36,15 +47,15 @@ export declare class ReadCache {
36
47
  private missCount;
37
48
  /**
38
49
  * Checks whether a file's content is already cached.
39
- * @param filePath - Relative file path from workspace root.
50
+ * @param keyOrPath - Relative file path or computed cache key.
40
51
  */
41
- has(filePath: string): boolean;
52
+ has(keyOrPath: string): boolean;
42
53
  /**
43
- * Retrieves cached content for a file.
44
- * @param filePath - Relative file path from workspace root.
54
+ * Retrieves cached content for a file or key.
55
+ * @param keyOrPath - Relative file path or computed cache key.
45
56
  * @returns The file content, or undefined if not cached.
46
57
  */
47
- get(filePath: string): {
58
+ get(keyOrPath: string): {
48
59
  text: string;
49
60
  inlineData?: any;
50
61
  } | undefined;
@@ -52,13 +63,24 @@ export declare class ReadCache {
52
63
  * Stores file content in the cache. If adding this entry would exceed
53
64
  * the memory cap, older entries are evicted in insertion order (LRU).
54
65
  *
55
- * @param filePath - Relative file path from workspace root.
66
+ * @param keyOrPath - Relative file path or computed cache key.
56
67
  * @param content - The file's text content.
57
68
  */
58
- set(filePath: string, content: {
69
+ set(keyOrPath: string, content: {
59
70
  text: string;
60
71
  inlineData?: any;
61
72
  }): void;
73
+ /**
74
+ * Invalidates and evicts all cache entries associated with a file path,
75
+ * including full-file reads and any ranged or targeted sub-reads.
76
+ *
77
+ * @param filePath - The file path that was modified or deleted.
78
+ */
79
+ invalidate(filePath: string): void;
80
+ /**
81
+ * Clears all cached file entries and resets byte counts.
82
+ */
83
+ clear(): void;
62
84
  /**
63
85
  * Returns all cached file entries. Used by the Reducer to assemble
64
86
  * the merged file set without re-reading from disk.
@@ -21,17 +21,38 @@ import { getMetricCollector } from '../metrics.js';
21
21
  // ─── Constants ───────────────────────────────────────────────────────
22
22
  /** Maximum total bytes of cached file content before LRU eviction kicks in. */
23
23
  const MAX_CACHE_BYTES = 5 * 1024 * 1024; // 5 MB
24
+ // ─── Key Computation Helper ──────────────────────────────────────────
25
+ /**
26
+ * Computes a standardized cache key for full-file or ranged/targeted read requests.
27
+ *
28
+ * @param filePath - Target relative or aliased file path.
29
+ * @param startLine - Optional 1-indexed starting line.
30
+ * @param endLine - Optional 1-indexed ending line.
31
+ * @param targetElements - Optional array of AST symbol names.
32
+ * @returns Serialized cache key string.
33
+ */
34
+ export function computeReadCacheKey(filePath, startLine, endLine, targetElements) {
35
+ const hasLines = startLine !== undefined || endLine !== undefined;
36
+ const hasElems = Array.isArray(targetElements) && targetElements.length > 0;
37
+ if (!hasLines && !hasElems) {
38
+ return filePath;
39
+ }
40
+ const linePart = hasLines ? `::lines=${startLine || 1}-${endLine || 'end'}` : '';
41
+ const elemPart = hasElems ? `::elems=${[...targetElements].sort().join(',')}` : '';
42
+ return `${filePath}${linePart}${elemPart}`;
43
+ }
24
44
  // ─── Read Cache Implementation ───────────────────────────────────────
25
45
  /**
26
- * In-memory file content cache shared across parallel investigation agents.
46
+ * In-memory file content cache shared across parallel investigation agents
47
+ * or scoped to individual execution sub-agents.
27
48
  *
28
49
  * Usage:
29
50
  * ```ts
30
51
  * const cache = new ReadCache()
31
- * // In tool wrapper:
32
- * if (cache.has(filePath)) return cache.get(filePath)!
33
- * const content = await fs.readFile(absPath, 'utf-8')
34
- * cache.set(filePath, content)
52
+ * const key = computeReadCacheKey(filePath, startLine, endLine)
53
+ * if (cache.has(key)) return cache.get(key)!
54
+ * const content = await readFile(root, filePath, startLine, endLine)
55
+ * cache.set(key, content)
35
56
  * ```
36
57
  */
37
58
  export class ReadCache {
@@ -42,10 +63,10 @@ export class ReadCache {
42
63
  missCount = 0;
43
64
  /**
44
65
  * Checks whether a file's content is already cached.
45
- * @param filePath - Relative file path from workspace root.
66
+ * @param keyOrPath - Relative file path or computed cache key.
46
67
  */
47
- has(filePath) {
48
- const found = this.cache.has(filePath);
68
+ has(keyOrPath) {
69
+ const found = this.cache.has(keyOrPath);
49
70
  const collector = getMetricCollector();
50
71
  if (found) {
51
72
  this.hitCount++;
@@ -58,23 +79,23 @@ export class ReadCache {
58
79
  return found;
59
80
  }
60
81
  /**
61
- * Retrieves cached content for a file.
62
- * @param filePath - Relative file path from workspace root.
82
+ * Retrieves cached content for a file or key.
83
+ * @param keyOrPath - Relative file path or computed cache key.
63
84
  * @returns The file content, or undefined if not cached.
64
85
  */
65
- get(filePath) {
66
- return this.cache.get(filePath);
86
+ get(keyOrPath) {
87
+ return this.cache.get(keyOrPath);
67
88
  }
68
89
  /**
69
90
  * Stores file content in the cache. If adding this entry would exceed
70
91
  * the memory cap, older entries are evicted in insertion order (LRU).
71
92
  *
72
- * @param filePath - Relative file path from workspace root.
93
+ * @param keyOrPath - Relative file path or computed cache key.
73
94
  * @param content - The file's text content.
74
95
  */
75
- set(filePath, content) {
96
+ set(keyOrPath, content) {
76
97
  // Don't re-insert if already cached
77
- if (this.cache.has(filePath))
98
+ if (this.cache.has(keyOrPath))
78
99
  return;
79
100
  const entryBytes = Buffer.byteLength(content.text, 'utf-8');
80
101
  // Evict oldest entries until there is room
@@ -87,10 +108,47 @@ export class ReadCache {
87
108
  debugLog(`ReadCache: Evicted "${oldest}" to stay under ${MAX_CACHE_BYTES} byte cap.`);
88
109
  }
89
110
  }
90
- this.cache.set(filePath, content);
91
- this.insertionOrder.push(filePath);
111
+ this.cache.set(keyOrPath, content);
112
+ this.insertionOrder.push(keyOrPath);
92
113
  this.totalBytes += entryBytes;
93
114
  }
115
+ /**
116
+ * Invalidates and evicts all cache entries associated with a file path,
117
+ * including full-file reads and any ranged or targeted sub-reads.
118
+ *
119
+ * @param filePath - The file path that was modified or deleted.
120
+ */
121
+ invalidate(filePath) {
122
+ if (!filePath)
123
+ return;
124
+ const prefix = `${filePath}::`;
125
+ const keysToRemove = [];
126
+ for (const key of this.cache.keys()) {
127
+ if (key === filePath || key.startsWith(prefix)) {
128
+ keysToRemove.push(key);
129
+ }
130
+ }
131
+ for (const key of keysToRemove) {
132
+ const evicted = this.cache.get(key);
133
+ if (evicted !== undefined) {
134
+ this.totalBytes -= Buffer.byteLength(evicted.text, 'utf-8');
135
+ this.cache.delete(key);
136
+ debugLog(`ReadCache: Invalidated "${key}"`);
137
+ }
138
+ const idx = this.insertionOrder.indexOf(key);
139
+ if (idx !== -1) {
140
+ this.insertionOrder.splice(idx, 1);
141
+ }
142
+ }
143
+ }
144
+ /**
145
+ * Clears all cached file entries and resets byte counts.
146
+ */
147
+ clear() {
148
+ this.cache.clear();
149
+ this.insertionOrder = [];
150
+ this.totalBytes = 0;
151
+ }
94
152
  /**
95
153
  * Returns all cached file entries. Used by the Reducer to assemble
96
154
  * the merged file set without re-reading from disk.
@@ -1,5 +1,6 @@
1
1
  import { MessageBus } from './messageBus.js';
2
2
  import { FileLockRegistry } from './fileLockRegistry.js';
3
+ import { ReadCache } from './readCache.js';
3
4
  import { type PathResolutionOptions } from '../../utils/pathSecurity.js';
4
5
  /**
5
6
  * Resolves a file path to its canonical absolute path for lock registry keying,
@@ -25,7 +26,7 @@ export declare function getScopedToolDeclarations(): any[];
25
26
  /**
26
27
  * Executes a tool within the sub-agent execution boundary.
27
28
  * Handles concurrency locking for file-mutating operations, sends progress
28
- * notifications to the orchestrator, and logs activity to the message bus.
29
+ * notifications to the orchestrator, checks per-agent read caches, and logs activity to the message bus.
29
30
  *
30
31
  * @param name - Tool function name
31
32
  * @param args - Tool arguments object
@@ -36,5 +37,6 @@ export declare function getScopedToolDeclarations(): any[];
36
37
  * @param onProgress - Callback to notify parent of sub-agent progress
37
38
  * @param options - Optional sub-path auto-focus and override configuration
38
39
  * @param taskScope - Optional task scope metadata (targetFiles, dependsOn)
40
+ * @param readCache - Optional per-agent in-memory file read cache
39
41
  */
40
- export declare function executeScopedTool(name: string, args: Record<string, any>, workspaceRoot: string, agentId: string, bus: MessageBus, locks: FileLockRegistry, onProgress: (msg?: string) => void, options?: PathResolutionOptions, taskScope?: TaskScope): Promise<any>;
42
+ export declare function executeScopedTool(name: string, args: Record<string, any>, workspaceRoot: string, agentId: string, bus: MessageBus, locks: FileLockRegistry, onProgress: (msg?: string) => void, options?: PathResolutionOptions, taskScope?: TaskScope, readCache?: ReadCache): Promise<any>;