File size: 3,945 Bytes
c2c8c8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import crypto from 'crypto';
import { AgentResult, AgentOptions, Finding, AgentSummary, AgentType, estimateTokens } from '@glmpilot/shared';
import { GLMClient, glm } from '../services/glm-client.js';
import { cache, CacheService } from '../services/cache.service.js';
import { logger } from '../utils/logger.js';
import { parseJSONSafe } from '../utils/code-parser.js';
import { isTestFile, isConfigFile, isSourceFile } from '../utils/file-utils.js';

export abstract class BaseAgent {
  abstract readonly name: AgentType;
  abstract readonly description: string;
  protected abstract readonly systemPrompt: string;

  protected glm: GLMClient = glm;
  protected cache = cache;

  abstract analyze(files: Map<string, string>, options?: AgentOptions): Promise<AgentResult>;

  protected buildFileContext(files: Map<string, string>): string {
    const entries = Array.from(files.entries());
    let totalEstimatedTokens = 0;
    const maxTokens = 28000;

    // Prioritize: source files first, then others, exclude tests/configs last
    const prioritized = entries.sort(([pathA], [pathB]) => {
      const aIsSource = isSourceFile(pathA) ? 0 : 1;
      const bIsSource = isSourceFile(pathB) ? 0 : 1;
      if (aIsSource !== bIsSource) return aIsSource - bIsSource;
      const aIsTest = isTestFile(pathA) ? 1 : 0;
      const bIsTest = isTestFile(pathB) ? 1 : 0;
      return aIsTest - bIsTest;
    });

    const parts: string[] = [];

    for (const [path, content] of prioritized) {
      let fileContent = content;
      const tokenEstimate = estimateTokens(fileContent);

      if (totalEstimatedTokens + tokenEstimate > maxTokens) {
        // Drop test files and config files first
        if (isTestFile(path) || isConfigFile(path)) continue;
        // Truncate large files
        const lines = fileContent.split('\n');
        if (lines.length > 200) {
          fileContent = lines.slice(0, 200).join('\n') + '\n// ... [truncated]';
        }
        const truncatedTokens = estimateTokens(fileContent);
        if (totalEstimatedTokens + truncatedTokens > maxTokens) continue;
        totalEstimatedTokens += truncatedTokens;
      } else {
        totalEstimatedTokens += tokenEstimate;
      }

      parts.push(`=== FILE: ${path} ===\n${fileContent}\n`);
    }

    return parts.join('\n');
  }

  protected parseJSONResponse<T>(raw: string, fallback: T): T {
    return parseJSONSafe(raw, fallback);
  }

  protected async analyzeWithCache(
    cacheKeySuffix: string,
    files: Map<string, string>,
    instruction: string
  ): Promise<string> {
    const concatenated = Array.from(files.values()).join('');
    const key = CacheService.hashKey(this.name, cacheKeySuffix, concatenated);

    return cache.getCachedOrCompute(
      `agent:${key}`,
      () => {
        const fileContext = this.buildFileContext(files);
        return this.glm.analyzeCode(fileContext, instruction, this.systemPrompt);
      },
      3600
    );
  }

  protected buildResult(raw: string): AgentResult {
    const fallback = { findings: [], summary: { critical: 0, high: 0, medium: 0, low: 0 }, overallRiskScore: 0 };
    const parsed = this.parseJSONResponse<{ findings?: Finding[]; summary?: AgentSummary; overallRiskScore?: number }>(raw, fallback);

    const findings: Finding[] = (parsed.findings || []).map((f, i) => ({
      ...f,
      id: f.id || `${this.name.toUpperCase()}-${String(i + 1).padStart(3, '0')}`,
      agent: this.name,
    }));

    const summary: AgentSummary = parsed.summary || {
      critical: findings.filter(f => f.severity === 'critical').length,
      high: findings.filter(f => f.severity === 'high').length,
      medium: findings.filter(f => f.severity === 'medium').length,
      low: findings.filter(f => f.severity === 'low').length,
    };

    return {
      agent: this.name,
      findings,
      summary,
      overallRiskScore: parsed.overallRiskScore || 0,
      executionTimeMs: 0,
    };
  }
}