minovative-mind-cli 2.13.2 → 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.
- package/dist/services/ai.d.ts +6 -2
- package/dist/services/ai.js +18 -7
- package/dist/services/contextAgent.js +32 -22
- package/dist/services/orchestration/investigationAgent.js +40 -9
- package/dist/services/orchestration/readCache.d.ts +34 -12
- package/dist/services/orchestration/readCache.js +75 -17
- package/dist/services/orchestration/scopedTools.d.ts +4 -2
- package/dist/services/orchestration/scopedTools.js +105 -50
- package/dist/services/orchestration/subAgent.d.ts +1 -0
- package/dist/services/orchestration/subAgent.js +24 -8
- package/dist/utils/localSyntaxValidator.d.ts +29 -2
- package/dist/utils/localSyntaxValidator.js +143 -78
- package/dist/utils/systemPrompts.d.ts +1 -1
- package/dist/utils/systemPrompts.js +6 -3
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
package/dist/services/ai.d.ts
CHANGED
|
@@ -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;
|
package/dist/services/ai.js
CHANGED
|
@@ -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, //
|
|
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, //
|
|
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 {
|
|
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
|
-
// ───
|
|
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
|
-
|
|
493
|
-
|
|
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.
|
|
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,16 +116,16 @@ 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
|
-
|
|
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();
|
|
121
|
-
if (onProgress) {
|
|
122
|
-
onProgress(`[${this.agentLabel}] Thinking...`);
|
|
123
|
-
}
|
|
124
129
|
let result;
|
|
125
130
|
try {
|
|
126
131
|
result = await this.chat.sendMessage(currentMessage, undefined, abortSignal, () => this.pingHeartbeat());
|
|
@@ -133,8 +138,34 @@ export class InvestigationAgentRunner {
|
|
|
133
138
|
this.updateUsage(result);
|
|
134
139
|
const functionCalls = result.response.functionCalls();
|
|
135
140
|
if (!functionCalls || functionCalls.length === 0) {
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
}
|
|
138
169
|
success = true;
|
|
139
170
|
break;
|
|
140
171
|
}
|
|
@@ -17,15 +17,26 @@
|
|
|
17
17
|
* to prevent OOM on very large monorepos.
|
|
18
18
|
*/
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
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
|
-
*
|
|
26
|
-
* if (cache.has(
|
|
27
|
-
* const content = await
|
|
28
|
-
* cache.set(
|
|
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
|
|
50
|
+
* @param keyOrPath - Relative file path or computed cache key.
|
|
40
51
|
*/
|
|
41
|
-
has(
|
|
52
|
+
has(keyOrPath: string): boolean;
|
|
42
53
|
/**
|
|
43
|
-
* Retrieves cached content for a file.
|
|
44
|
-
* @param
|
|
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(
|
|
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
|
|
66
|
+
* @param keyOrPath - Relative file path or computed cache key.
|
|
56
67
|
* @param content - The file's text content.
|
|
57
68
|
*/
|
|
58
|
-
set(
|
|
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
|
-
*
|
|
32
|
-
* if (cache.has(
|
|
33
|
-
* const content = await
|
|
34
|
-
* cache.set(
|
|
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
|
|
66
|
+
* @param keyOrPath - Relative file path or computed cache key.
|
|
46
67
|
*/
|
|
47
|
-
has(
|
|
48
|
-
const found = this.cache.has(
|
|
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
|
|
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(
|
|
66
|
-
return this.cache.get(
|
|
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
|
|
93
|
+
* @param keyOrPath - Relative file path or computed cache key.
|
|
73
94
|
* @param content - The file's text content.
|
|
74
95
|
*/
|
|
75
|
-
set(
|
|
96
|
+
set(keyOrPath, content) {
|
|
76
97
|
// Don't re-insert if already cached
|
|
77
|
-
if (this.cache.has(
|
|
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(
|
|
91
|
-
this.insertionOrder.push(
|
|
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>;
|