@openqa/cli 1.3.4 → 2.0.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 +1 -1
- package/dist/agent/brain/diff-analyzer.js +140 -0
- package/dist/agent/brain/diff-analyzer.js.map +1 -0
- package/dist/agent/brain/llm-cache.js +47 -0
- package/dist/agent/brain/llm-cache.js.map +1 -0
- package/dist/agent/brain/llm-resilience.js +252 -0
- package/dist/agent/brain/llm-resilience.js.map +1 -0
- package/dist/agent/config/index.js +588 -0
- package/dist/agent/config/index.js.map +1 -0
- package/dist/agent/coverage/index.js +74 -0
- package/dist/agent/coverage/index.js.map +1 -0
- package/dist/agent/export/index.js +158 -0
- package/dist/agent/export/index.js.map +1 -0
- package/dist/agent/index-v2.js +2795 -0
- package/dist/agent/index-v2.js.map +1 -0
- package/dist/agent/index.js +369 -105
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/logger.js +41 -0
- package/dist/agent/logger.js.map +1 -0
- package/dist/agent/metrics.js +39 -0
- package/dist/agent/metrics.js.map +1 -0
- package/dist/agent/notifications/index.js +106 -0
- package/dist/agent/notifications/index.js.map +1 -0
- package/dist/agent/openapi/spec.js +338 -0
- package/dist/agent/openapi/spec.js.map +1 -0
- package/dist/agent/tools/project-runner.js +481 -0
- package/dist/agent/tools/project-runner.js.map +1 -0
- package/dist/cli/config.html.js +454 -0
- package/dist/cli/daemon.js +7572 -0
- package/dist/cli/dashboard.html.js +1619 -0
- package/dist/cli/index.js +3492 -1622
- package/dist/cli/kanban.html.js +577 -0
- package/dist/cli/routes.js +895 -0
- package/dist/cli/routes.js.map +1 -0
- package/dist/cli/server.js +3469 -1630
- package/dist/database/index.js +485 -60
- package/dist/database/index.js.map +1 -1
- package/dist/database/sqlite.js +281 -0
- package/dist/database/sqlite.js.map +1 -0
- package/package.json +18 -5
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<h1 align="center">OpenQA</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>Autonomous QA Testing Agent - Thinks, codes, and executes tests like a senior QA engineer. Powered by Orka
|
|
8
|
+
<strong>Autonomous QA Testing Agent - Thinks, codes, and executes tests like a senior QA engineer. Powered by Orka Team</strong>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// agent/brain/diff-analyzer.ts
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { join, basename, dirname } from "path";
|
|
5
|
+
var DiffAnalyzer = class {
|
|
6
|
+
/**
|
|
7
|
+
* Get files changed between current branch and base branch
|
|
8
|
+
*/
|
|
9
|
+
getChangedFiles(repoPath, baseBranch = "main") {
|
|
10
|
+
try {
|
|
11
|
+
const output = execSync(`git diff --name-only ${baseBranch}...HEAD`, {
|
|
12
|
+
cwd: repoPath,
|
|
13
|
+
stdio: "pipe"
|
|
14
|
+
}).toString().trim();
|
|
15
|
+
if (!output) return [];
|
|
16
|
+
return output.split("\n").filter(Boolean);
|
|
17
|
+
} catch {
|
|
18
|
+
try {
|
|
19
|
+
const output = execSync("git diff --name-only HEAD~1", {
|
|
20
|
+
cwd: repoPath,
|
|
21
|
+
stdio: "pipe"
|
|
22
|
+
}).toString().trim();
|
|
23
|
+
if (!output) return [];
|
|
24
|
+
return output.split("\n").filter(Boolean);
|
|
25
|
+
} catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Map changed source files to their likely test files
|
|
32
|
+
*/
|
|
33
|
+
mapFilesToTests(changedFiles, repoPath) {
|
|
34
|
+
const testFiles = /* @__PURE__ */ new Set();
|
|
35
|
+
for (const file of changedFiles) {
|
|
36
|
+
if (!this.isSourceFile(file)) continue;
|
|
37
|
+
const candidates = this.getTestCandidates(file);
|
|
38
|
+
for (const candidate of candidates) {
|
|
39
|
+
if (existsSync(join(repoPath, candidate))) {
|
|
40
|
+
testFiles.add(candidate);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const file of changedFiles) {
|
|
45
|
+
if (this.isTestFile(file)) {
|
|
46
|
+
testFiles.add(file);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return Array.from(testFiles);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Analyze a diff and return risk assessment + affected tests
|
|
53
|
+
*/
|
|
54
|
+
analyze(repoPath, baseBranch = "main") {
|
|
55
|
+
const changedFiles = this.getChangedFiles(repoPath, baseBranch);
|
|
56
|
+
const affectedTests = this.mapFilesToTests(changedFiles, repoPath);
|
|
57
|
+
const riskLevel = this.assessRisk(changedFiles);
|
|
58
|
+
const summary = this.buildSummary(changedFiles, affectedTests, riskLevel);
|
|
59
|
+
return { changedFiles, affectedTests, riskLevel, summary };
|
|
60
|
+
}
|
|
61
|
+
getTestCandidates(filePath) {
|
|
62
|
+
const dir = dirname(filePath);
|
|
63
|
+
const base = basename(filePath);
|
|
64
|
+
const candidates = [];
|
|
65
|
+
const nameMatch = base.match(/^(.+)\.(tsx?|jsx?|vue|svelte|py|go|rs)$/);
|
|
66
|
+
if (!nameMatch) return candidates;
|
|
67
|
+
const name = nameMatch[1];
|
|
68
|
+
const ext = nameMatch[2];
|
|
69
|
+
const testExts = ext.startsWith("ts") ? ["test.ts", "test.tsx", "spec.ts", "spec.tsx"] : ext.startsWith("js") ? ["test.js", "test.jsx", "spec.js", "spec.jsx"] : ext === "py" ? ["test.py"] : ext === "go" ? ["_test.go"] : [];
|
|
70
|
+
for (const testExt of testExts) {
|
|
71
|
+
candidates.push(join(dir, `${name}.${testExt}`));
|
|
72
|
+
candidates.push(join(dir, "__tests__", `${name}.${testExt}`));
|
|
73
|
+
candidates.push(join(dir, "test", `${name}.${testExt}`));
|
|
74
|
+
candidates.push(join(dir, "tests", `${name}.${testExt}`));
|
|
75
|
+
candidates.push(join("__tests__", dir, `${name}.${testExt}`));
|
|
76
|
+
}
|
|
77
|
+
if (ext === "go") {
|
|
78
|
+
candidates.push(join(dir, `${name}_test.go`));
|
|
79
|
+
}
|
|
80
|
+
return candidates;
|
|
81
|
+
}
|
|
82
|
+
isSourceFile(file) {
|
|
83
|
+
return /\.(tsx?|jsx?|vue|svelte|py|go|rs)$/.test(file) && !this.isTestFile(file);
|
|
84
|
+
}
|
|
85
|
+
isTestFile(file) {
|
|
86
|
+
return /\.(test|spec)\.(tsx?|jsx?|py)$/.test(file) || /_test\.go$/.test(file) || file.includes("__tests__/");
|
|
87
|
+
}
|
|
88
|
+
assessRisk(changedFiles) {
|
|
89
|
+
const highRiskPatterns = [
|
|
90
|
+
/auth/i,
|
|
91
|
+
/security/i,
|
|
92
|
+
/middleware/i,
|
|
93
|
+
/database/i,
|
|
94
|
+
/migration/i,
|
|
95
|
+
/config/i,
|
|
96
|
+
/\.env/,
|
|
97
|
+
/package\.json$/,
|
|
98
|
+
/docker/i,
|
|
99
|
+
/ci\//i,
|
|
100
|
+
/payment/i,
|
|
101
|
+
/billing/i,
|
|
102
|
+
/permission/i
|
|
103
|
+
];
|
|
104
|
+
const mediumRiskPatterns = [
|
|
105
|
+
/api/i,
|
|
106
|
+
/route/i,
|
|
107
|
+
/controller/i,
|
|
108
|
+
/service/i,
|
|
109
|
+
/model/i,
|
|
110
|
+
/hook/i,
|
|
111
|
+
/context/i,
|
|
112
|
+
/store/i,
|
|
113
|
+
/util/i
|
|
114
|
+
];
|
|
115
|
+
let highCount = 0;
|
|
116
|
+
let mediumCount = 0;
|
|
117
|
+
for (const file of changedFiles) {
|
|
118
|
+
if (highRiskPatterns.some((p) => p.test(file))) highCount++;
|
|
119
|
+
else if (mediumRiskPatterns.some((p) => p.test(file))) mediumCount++;
|
|
120
|
+
}
|
|
121
|
+
if (highCount >= 2 || highCount >= 1 && changedFiles.length > 5) return "high";
|
|
122
|
+
if (highCount >= 1 || mediumCount >= 3) return "medium";
|
|
123
|
+
return "low";
|
|
124
|
+
}
|
|
125
|
+
buildSummary(changedFiles, affectedTests, riskLevel) {
|
|
126
|
+
const lines = [];
|
|
127
|
+
lines.push(`${changedFiles.length} file(s) changed, ${affectedTests.length} test(s) affected.`);
|
|
128
|
+
lines.push(`Risk level: ${riskLevel}.`);
|
|
129
|
+
if (affectedTests.length > 0) {
|
|
130
|
+
lines.push(`Run: ${affectedTests.slice(0, 5).join(", ")}${affectedTests.length > 5 ? ` (+${affectedTests.length - 5} more)` : ""}`);
|
|
131
|
+
} else if (changedFiles.length > 0) {
|
|
132
|
+
lines.push("No matching test files found \u2014 consider running full suite.");
|
|
133
|
+
}
|
|
134
|
+
return lines.join(" ");
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
export {
|
|
138
|
+
DiffAnalyzer
|
|
139
|
+
};
|
|
140
|
+
//# sourceMappingURL=diff-analyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../agent/brain/diff-analyzer.ts"],"sourcesContent":["import { execSync } from 'child_process';\nimport { existsSync } from 'fs';\nimport { join, basename, dirname } from 'path';\n\nexport interface DiffResult {\n changedFiles: string[];\n affectedTests: string[];\n riskLevel: 'low' | 'medium' | 'high';\n summary: string;\n}\n\nexport class DiffAnalyzer {\n /**\n * Get files changed between current branch and base branch\n */\n getChangedFiles(repoPath: string, baseBranch: string = 'main'): string[] {\n try {\n const output = execSync(`git diff --name-only ${baseBranch}...HEAD`, {\n cwd: repoPath,\n stdio: 'pipe',\n }).toString().trim();\n\n if (!output) return [];\n return output.split('\\n').filter(Boolean);\n } catch {\n // Fallback: diff against HEAD~1 if base branch doesn't exist\n try {\n const output = execSync('git diff --name-only HEAD~1', {\n cwd: repoPath,\n stdio: 'pipe',\n }).toString().trim();\n\n if (!output) return [];\n return output.split('\\n').filter(Boolean);\n } catch {\n return [];\n }\n }\n }\n\n /**\n * Map changed source files to their likely test files\n */\n mapFilesToTests(changedFiles: string[], repoPath: string): string[] {\n const testFiles = new Set<string>();\n\n for (const file of changedFiles) {\n // Skip non-source files\n if (!this.isSourceFile(file)) continue;\n\n const candidates = this.getTestCandidates(file);\n for (const candidate of candidates) {\n if (existsSync(join(repoPath, candidate))) {\n testFiles.add(candidate);\n }\n }\n }\n\n // Also include any changed test files directly\n for (const file of changedFiles) {\n if (this.isTestFile(file)) {\n testFiles.add(file);\n }\n }\n\n return Array.from(testFiles);\n }\n\n /**\n * Analyze a diff and return risk assessment + affected tests\n */\n analyze(repoPath: string, baseBranch: string = 'main'): DiffResult {\n const changedFiles = this.getChangedFiles(repoPath, baseBranch);\n const affectedTests = this.mapFilesToTests(changedFiles, repoPath);\n\n const riskLevel = this.assessRisk(changedFiles);\n\n const summary = this.buildSummary(changedFiles, affectedTests, riskLevel);\n\n return { changedFiles, affectedTests, riskLevel, summary };\n }\n\n private getTestCandidates(filePath: string): string[] {\n const dir = dirname(filePath);\n const base = basename(filePath);\n const candidates: string[] = [];\n\n // Remove extension\n const nameMatch = base.match(/^(.+)\\.(tsx?|jsx?|vue|svelte|py|go|rs)$/);\n if (!nameMatch) return candidates;\n const name = nameMatch[1];\n const ext = nameMatch[2];\n\n // Common test file patterns\n const testExts = ext.startsWith('ts') ? ['test.ts', 'test.tsx', 'spec.ts', 'spec.tsx']\n : ext.startsWith('js') ? ['test.js', 'test.jsx', 'spec.js', 'spec.jsx']\n : ext === 'py' ? ['test.py']\n : ext === 'go' ? ['_test.go']\n : [];\n\n for (const testExt of testExts) {\n // Same directory: Foo.test.ts\n candidates.push(join(dir, `${name}.${testExt}`));\n // __tests__ directory: __tests__/Foo.test.ts\n candidates.push(join(dir, '__tests__', `${name}.${testExt}`));\n // test/ directory: test/Foo.test.ts\n candidates.push(join(dir, 'test', `${name}.${testExt}`));\n // tests/ directory: tests/Foo.test.ts\n candidates.push(join(dir, 'tests', `${name}.${testExt}`));\n // Root __tests__: __tests__/dir/Foo.test.ts\n candidates.push(join('__tests__', dir, `${name}.${testExt}`));\n }\n\n // Go convention: same file with _test suffix\n if (ext === 'go') {\n candidates.push(join(dir, `${name}_test.go`));\n }\n\n return candidates;\n }\n\n private isSourceFile(file: string): boolean {\n return /\\.(tsx?|jsx?|vue|svelte|py|go|rs)$/.test(file) && !this.isTestFile(file);\n }\n\n private isTestFile(file: string): boolean {\n return /\\.(test|spec)\\.(tsx?|jsx?|py)$/.test(file) ||\n /_test\\.go$/.test(file) ||\n file.includes('__tests__/');\n }\n\n private assessRisk(changedFiles: string[]): 'low' | 'medium' | 'high' {\n const highRiskPatterns = [\n /auth/i, /security/i, /middleware/i, /database/i, /migration/i,\n /config/i, /\\.env/, /package\\.json$/, /docker/i, /ci\\//i,\n /payment/i, /billing/i, /permission/i,\n ];\n\n const mediumRiskPatterns = [\n /api/i, /route/i, /controller/i, /service/i, /model/i,\n /hook/i, /context/i, /store/i, /util/i,\n ];\n\n let highCount = 0;\n let mediumCount = 0;\n\n for (const file of changedFiles) {\n if (highRiskPatterns.some(p => p.test(file))) highCount++;\n else if (mediumRiskPatterns.some(p => p.test(file))) mediumCount++;\n }\n\n if (highCount >= 2 || (highCount >= 1 && changedFiles.length > 5)) return 'high';\n if (highCount >= 1 || mediumCount >= 3) return 'medium';\n return 'low';\n }\n\n private buildSummary(changedFiles: string[], affectedTests: string[], riskLevel: string): string {\n const lines: string[] = [];\n lines.push(`${changedFiles.length} file(s) changed, ${affectedTests.length} test(s) affected.`);\n lines.push(`Risk level: ${riskLevel}.`);\n\n if (affectedTests.length > 0) {\n lines.push(`Run: ${affectedTests.slice(0, 5).join(', ')}${affectedTests.length > 5 ? ` (+${affectedTests.length - 5} more)` : ''}`);\n } else if (changedFiles.length > 0) {\n lines.push('No matching test files found — consider running full suite.');\n }\n\n return lines.join(' ');\n }\n}\n"],"mappings":";AAAA,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAC3B,SAAS,MAAM,UAAU,eAAe;AASjC,IAAM,eAAN,MAAmB;AAAA;AAAA;AAAA;AAAA,EAIxB,gBAAgB,UAAkB,aAAqB,QAAkB;AACvE,QAAI;AACF,YAAM,SAAS,SAAS,wBAAwB,UAAU,WAAW;AAAA,QACnE,KAAK;AAAA,QACL,OAAO;AAAA,MACT,CAAC,EAAE,SAAS,EAAE,KAAK;AAEnB,UAAI,CAAC,OAAQ,QAAO,CAAC;AACrB,aAAO,OAAO,MAAM,IAAI,EAAE,OAAO,OAAO;AAAA,IAC1C,QAAQ;AAEN,UAAI;AACF,cAAM,SAAS,SAAS,+BAA+B;AAAA,UACrD,KAAK;AAAA,UACL,OAAO;AAAA,QACT,CAAC,EAAE,SAAS,EAAE,KAAK;AAEnB,YAAI,CAAC,OAAQ,QAAO,CAAC;AACrB,eAAO,OAAO,MAAM,IAAI,EAAE,OAAO,OAAO;AAAA,MAC1C,QAAQ;AACN,eAAO,CAAC;AAAA,MACV;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,cAAwB,UAA4B;AAClE,UAAM,YAAY,oBAAI,IAAY;AAElC,eAAW,QAAQ,cAAc;AAE/B,UAAI,CAAC,KAAK,aAAa,IAAI,EAAG;AAE9B,YAAM,aAAa,KAAK,kBAAkB,IAAI;AAC9C,iBAAW,aAAa,YAAY;AAClC,YAAI,WAAW,KAAK,UAAU,SAAS,CAAC,GAAG;AACzC,oBAAU,IAAI,SAAS;AAAA,QACzB;AAAA,MACF;AAAA,IACF;AAGA,eAAW,QAAQ,cAAc;AAC/B,UAAI,KAAK,WAAW,IAAI,GAAG;AACzB,kBAAU,IAAI,IAAI;AAAA,MACpB;AAAA,IACF;AAEA,WAAO,MAAM,KAAK,SAAS;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA,EAKA,QAAQ,UAAkB,aAAqB,QAAoB;AACjE,UAAM,eAAe,KAAK,gBAAgB,UAAU,UAAU;AAC9D,UAAM,gBAAgB,KAAK,gBAAgB,cAAc,QAAQ;AAEjE,UAAM,YAAY,KAAK,WAAW,YAAY;AAE9C,UAAM,UAAU,KAAK,aAAa,cAAc,eAAe,SAAS;AAExE,WAAO,EAAE,cAAc,eAAe,WAAW,QAAQ;AAAA,EAC3D;AAAA,EAEQ,kBAAkB,UAA4B;AACpD,UAAM,MAAM,QAAQ,QAAQ;AAC5B,UAAM,OAAO,SAAS,QAAQ;AAC9B,UAAM,aAAuB,CAAC;AAG9B,UAAM,YAAY,KAAK,MAAM,yCAAyC;AACtE,QAAI,CAAC,UAAW,QAAO;AACvB,UAAM,OAAO,UAAU,CAAC;AACxB,UAAM,MAAM,UAAU,CAAC;AAGvB,UAAM,WAAW,IAAI,WAAW,IAAI,IAAI,CAAC,WAAW,YAAY,WAAW,UAAU,IACjF,IAAI,WAAW,IAAI,IAAI,CAAC,WAAW,YAAY,WAAW,UAAU,IACpE,QAAQ,OAAO,CAAC,SAAS,IACzB,QAAQ,OAAO,CAAC,UAAU,IAC1B,CAAC;AAEL,eAAW,WAAW,UAAU;AAE9B,iBAAW,KAAK,KAAK,KAAK,GAAG,IAAI,IAAI,OAAO,EAAE,CAAC;AAE/C,iBAAW,KAAK,KAAK,KAAK,aAAa,GAAG,IAAI,IAAI,OAAO,EAAE,CAAC;AAE5D,iBAAW,KAAK,KAAK,KAAK,QAAQ,GAAG,IAAI,IAAI,OAAO,EAAE,CAAC;AAEvD,iBAAW,KAAK,KAAK,KAAK,SAAS,GAAG,IAAI,IAAI,OAAO,EAAE,CAAC;AAExD,iBAAW,KAAK,KAAK,aAAa,KAAK,GAAG,IAAI,IAAI,OAAO,EAAE,CAAC;AAAA,IAC9D;AAGA,QAAI,QAAQ,MAAM;AAChB,iBAAW,KAAK,KAAK,KAAK,GAAG,IAAI,UAAU,CAAC;AAAA,IAC9C;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,aAAa,MAAuB;AAC1C,WAAO,qCAAqC,KAAK,IAAI,KAAK,CAAC,KAAK,WAAW,IAAI;AAAA,EACjF;AAAA,EAEQ,WAAW,MAAuB;AACxC,WAAO,iCAAiC,KAAK,IAAI,KAC/C,aAAa,KAAK,IAAI,KACtB,KAAK,SAAS,YAAY;AAAA,EAC9B;AAAA,EAEQ,WAAW,cAAmD;AACpE,UAAM,mBAAmB;AAAA,MACvB;AAAA,MAAS;AAAA,MAAa;AAAA,MAAe;AAAA,MAAa;AAAA,MAClD;AAAA,MAAW;AAAA,MAAS;AAAA,MAAkB;AAAA,MAAW;AAAA,MACjD;AAAA,MAAY;AAAA,MAAY;AAAA,IAC1B;AAEA,UAAM,qBAAqB;AAAA,MACzB;AAAA,MAAQ;AAAA,MAAU;AAAA,MAAe;AAAA,MAAY;AAAA,MAC7C;AAAA,MAAS;AAAA,MAAY;AAAA,MAAU;AAAA,IACjC;AAEA,QAAI,YAAY;AAChB,QAAI,cAAc;AAElB,eAAW,QAAQ,cAAc;AAC/B,UAAI,iBAAiB,KAAK,OAAK,EAAE,KAAK,IAAI,CAAC,EAAG;AAAA,eACrC,mBAAmB,KAAK,OAAK,EAAE,KAAK,IAAI,CAAC,EAAG;AAAA,IACvD;AAEA,QAAI,aAAa,KAAM,aAAa,KAAK,aAAa,SAAS,EAAI,QAAO;AAC1E,QAAI,aAAa,KAAK,eAAe,EAAG,QAAO;AAC/C,WAAO;AAAA,EACT;AAAA,EAEQ,aAAa,cAAwB,eAAyB,WAA2B;AAC/F,UAAM,QAAkB,CAAC;AACzB,UAAM,KAAK,GAAG,aAAa,MAAM,qBAAqB,cAAc,MAAM,oBAAoB;AAC9F,UAAM,KAAK,eAAe,SAAS,GAAG;AAEtC,QAAI,cAAc,SAAS,GAAG;AAC5B,YAAM,KAAK,QAAQ,cAAc,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,GAAG,cAAc,SAAS,IAAI,MAAM,cAAc,SAAS,CAAC,WAAW,EAAE,EAAE;AAAA,IACpI,WAAW,aAAa,SAAS,GAAG;AAClC,YAAM,KAAK,kEAA6D;AAAA,IAC1E;AAEA,WAAO,MAAM,KAAK,GAAG;AAAA,EACvB;AACF;","names":[]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// agent/brain/llm-cache.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
var LLMCache = class {
|
|
4
|
+
store = /* @__PURE__ */ new Map();
|
|
5
|
+
ttlMs;
|
|
6
|
+
maxSize;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.ttlMs = options?.ttlMs ?? 36e5;
|
|
9
|
+
this.maxSize = options?.maxSize ?? 500;
|
|
10
|
+
}
|
|
11
|
+
key(prompt) {
|
|
12
|
+
return createHash("sha256").update(prompt).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
get(prompt) {
|
|
15
|
+
const k = this.key(prompt);
|
|
16
|
+
const entry = this.store.get(k);
|
|
17
|
+
if (!entry) return null;
|
|
18
|
+
if (Date.now() > entry.expiresAt) {
|
|
19
|
+
this.store.delete(k);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return entry.response;
|
|
23
|
+
}
|
|
24
|
+
set(prompt, response) {
|
|
25
|
+
if (this.store.size >= this.maxSize) {
|
|
26
|
+
const oldest = this.store.keys().next().value;
|
|
27
|
+
if (oldest) this.store.delete(oldest);
|
|
28
|
+
}
|
|
29
|
+
this.store.set(this.key(prompt), {
|
|
30
|
+
response,
|
|
31
|
+
expiresAt: Date.now() + this.ttlMs
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
clear() {
|
|
35
|
+
this.store.clear();
|
|
36
|
+
}
|
|
37
|
+
get size() {
|
|
38
|
+
return this.store.size;
|
|
39
|
+
}
|
|
40
|
+
stats() {
|
|
41
|
+
return { size: this.store.size, ttlMs: this.ttlMs, maxSize: this.maxSize };
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
export {
|
|
45
|
+
LLMCache
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=llm-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../agent/brain/llm-cache.ts"],"sourcesContent":["import { createHash } from 'crypto';\n\ninterface CacheEntry {\n response: string;\n expiresAt: number;\n}\n\nexport class LLMCache {\n private store = new Map<string, CacheEntry>();\n private ttlMs: number;\n private maxSize: number;\n\n constructor(options?: { ttlMs?: number; maxSize?: number }) {\n this.ttlMs = options?.ttlMs ?? 3600000; // 1 hour\n this.maxSize = options?.maxSize ?? 500;\n }\n\n private key(prompt: string): string {\n return createHash('sha256').update(prompt).digest('hex');\n }\n\n get(prompt: string): string | null {\n const k = this.key(prompt);\n const entry = this.store.get(k);\n if (!entry) return null;\n if (Date.now() > entry.expiresAt) {\n this.store.delete(k);\n return null;\n }\n return entry.response;\n }\n\n set(prompt: string, response: string): void {\n if (this.store.size >= this.maxSize) {\n // Evict oldest entry\n const oldest = this.store.keys().next().value;\n if (oldest) this.store.delete(oldest);\n }\n this.store.set(this.key(prompt), {\n response,\n expiresAt: Date.now() + this.ttlMs,\n });\n }\n\n clear(): void {\n this.store.clear();\n }\n\n get size(): number {\n return this.store.size;\n }\n\n stats(): { size: number; ttlMs: number; maxSize: number } {\n return { size: this.store.size, ttlMs: this.ttlMs, maxSize: this.maxSize };\n }\n}\n"],"mappings":";AAAA,SAAS,kBAAkB;AAOpB,IAAM,WAAN,MAAe;AAAA,EACZ,QAAQ,oBAAI,IAAwB;AAAA,EACpC;AAAA,EACA;AAAA,EAER,YAAY,SAAgD;AAC1D,SAAK,QAAQ,SAAS,SAAS;AAC/B,SAAK,UAAU,SAAS,WAAW;AAAA,EACrC;AAAA,EAEQ,IAAI,QAAwB;AAClC,WAAO,WAAW,QAAQ,EAAE,OAAO,MAAM,EAAE,OAAO,KAAK;AAAA,EACzD;AAAA,EAEA,IAAI,QAA+B;AACjC,UAAM,IAAI,KAAK,IAAI,MAAM;AACzB,UAAM,QAAQ,KAAK,MAAM,IAAI,CAAC;AAC9B,QAAI,CAAC,MAAO,QAAO;AACnB,QAAI,KAAK,IAAI,IAAI,MAAM,WAAW;AAChC,WAAK,MAAM,OAAO,CAAC;AACnB,aAAO;AAAA,IACT;AACA,WAAO,MAAM;AAAA,EACf;AAAA,EAEA,IAAI,QAAgB,UAAwB;AAC1C,QAAI,KAAK,MAAM,QAAQ,KAAK,SAAS;AAEnC,YAAM,SAAS,KAAK,MAAM,KAAK,EAAE,KAAK,EAAE;AACxC,UAAI,OAAQ,MAAK,MAAM,OAAO,MAAM;AAAA,IACtC;AACA,SAAK,MAAM,IAAI,KAAK,IAAI,MAAM,GAAG;AAAA,MAC/B;AAAA,MACA,WAAW,KAAK,IAAI,IAAI,KAAK;AAAA,IAC/B,CAAC;AAAA,EACH;AAAA,EAEA,QAAc;AACZ,SAAK,MAAM,MAAM;AAAA,EACnB;AAAA,EAEA,IAAI,OAAe;AACjB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA,EAEA,QAA0D;AACxD,WAAO,EAAE,MAAM,KAAK,MAAM,MAAM,OAAO,KAAK,OAAO,SAAS,KAAK,QAAQ;AAAA,EAC3E;AACF;","names":[]}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// agent/brain/llm-resilience.ts
|
|
2
|
+
import { OpenAIAdapter } from "@orka-js/openai";
|
|
3
|
+
import { AnthropicAdapter } from "@orka-js/anthropic";
|
|
4
|
+
import { EventEmitter } from "events";
|
|
5
|
+
|
|
6
|
+
// agent/errors.ts
|
|
7
|
+
var OpenQAError = class extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(message, code) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "OpenQAError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var BrainError = class extends OpenQAError {
|
|
16
|
+
constructor(message, code = "BRAIN_ERROR") {
|
|
17
|
+
super(message, code);
|
|
18
|
+
this.name = "BrainError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// agent/brain/llm-cache.ts
|
|
23
|
+
import { createHash } from "crypto";
|
|
24
|
+
var LLMCache = class {
|
|
25
|
+
store = /* @__PURE__ */ new Map();
|
|
26
|
+
ttlMs;
|
|
27
|
+
maxSize;
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.ttlMs = options?.ttlMs ?? 36e5;
|
|
30
|
+
this.maxSize = options?.maxSize ?? 500;
|
|
31
|
+
}
|
|
32
|
+
key(prompt) {
|
|
33
|
+
return createHash("sha256").update(prompt).digest("hex");
|
|
34
|
+
}
|
|
35
|
+
get(prompt) {
|
|
36
|
+
const k = this.key(prompt);
|
|
37
|
+
const entry = this.store.get(k);
|
|
38
|
+
if (!entry) return null;
|
|
39
|
+
if (Date.now() > entry.expiresAt) {
|
|
40
|
+
this.store.delete(k);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return entry.response;
|
|
44
|
+
}
|
|
45
|
+
set(prompt, response) {
|
|
46
|
+
if (this.store.size >= this.maxSize) {
|
|
47
|
+
const oldest = this.store.keys().next().value;
|
|
48
|
+
if (oldest) this.store.delete(oldest);
|
|
49
|
+
}
|
|
50
|
+
this.store.set(this.key(prompt), {
|
|
51
|
+
response,
|
|
52
|
+
expiresAt: Date.now() + this.ttlMs
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
clear() {
|
|
56
|
+
this.store.clear();
|
|
57
|
+
}
|
|
58
|
+
get size() {
|
|
59
|
+
return this.store.size;
|
|
60
|
+
}
|
|
61
|
+
stats() {
|
|
62
|
+
return { size: this.store.size, ttlMs: this.ttlMs, maxSize: this.maxSize };
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// agent/metrics.ts
|
|
67
|
+
var startedAt = Date.now();
|
|
68
|
+
var counters = {
|
|
69
|
+
llm_calls: 0,
|
|
70
|
+
llm_cache_hits: 0,
|
|
71
|
+
llm_retries: 0,
|
|
72
|
+
llm_fallbacks: 0,
|
|
73
|
+
llm_circuit_opens: 0,
|
|
74
|
+
tests_generated: 0,
|
|
75
|
+
tests_run: 0,
|
|
76
|
+
tests_passed: 0,
|
|
77
|
+
tests_failed: 0,
|
|
78
|
+
bugs_found: 0,
|
|
79
|
+
sessions_started: 0,
|
|
80
|
+
ws_connections: 0,
|
|
81
|
+
http_requests: 0
|
|
82
|
+
};
|
|
83
|
+
var metrics = {
|
|
84
|
+
inc(key, by = 1) {
|
|
85
|
+
if (key in counters) counters[key] += by;
|
|
86
|
+
},
|
|
87
|
+
snapshot() {
|
|
88
|
+
const memMB = process.memoryUsage();
|
|
89
|
+
return {
|
|
90
|
+
uptimeSeconds: Math.floor((Date.now() - startedAt) / 1e3),
|
|
91
|
+
memory: {
|
|
92
|
+
heapUsedMB: Math.round(memMB.heapUsed / 1024 / 1024),
|
|
93
|
+
heapTotalMB: Math.round(memMB.heapTotal / 1024 / 1024),
|
|
94
|
+
rssMB: Math.round(memMB.rss / 1024 / 1024)
|
|
95
|
+
},
|
|
96
|
+
counters: { ...counters },
|
|
97
|
+
cacheHitRate: counters.llm_calls > 0 ? Math.round(counters.llm_cache_hits / (counters.llm_calls + counters.llm_cache_hits) * 100) : 0
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// agent/brain/llm-resilience.ts
|
|
103
|
+
var ResilientLLM = class extends EventEmitter {
|
|
104
|
+
config;
|
|
105
|
+
circuit = { failures: 0, lastFailure: 0, isOpen: false };
|
|
106
|
+
maxRetries;
|
|
107
|
+
circuitThreshold;
|
|
108
|
+
circuitResetMs;
|
|
109
|
+
cache;
|
|
110
|
+
constructor(config) {
|
|
111
|
+
super();
|
|
112
|
+
this.config = config;
|
|
113
|
+
this.maxRetries = config.maxRetries ?? 3;
|
|
114
|
+
this.circuitThreshold = config.circuitThreshold ?? 5;
|
|
115
|
+
this.circuitResetMs = config.circuitResetMs ?? 3e4;
|
|
116
|
+
this.cache = new LLMCache({
|
|
117
|
+
ttlMs: config.cacheTtlMs,
|
|
118
|
+
maxSize: config.cacheMaxSize
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
createAdapter(provider, apiKey, model) {
|
|
122
|
+
if (provider === "anthropic") {
|
|
123
|
+
return new AnthropicAdapter({
|
|
124
|
+
apiKey,
|
|
125
|
+
model: model || "claude-3-5-sonnet-20241022"
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return new OpenAIAdapter({
|
|
129
|
+
apiKey,
|
|
130
|
+
model: model || "gpt-4"
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
isCircuitOpen() {
|
|
134
|
+
if (!this.circuit.isOpen) return false;
|
|
135
|
+
if (Date.now() - this.circuit.lastFailure >= this.circuitResetMs) {
|
|
136
|
+
this.circuit.isOpen = false;
|
|
137
|
+
this.emit("llm-circuit-half-open", { provider: this.config.provider });
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
recordFailure() {
|
|
143
|
+
this.circuit.failures++;
|
|
144
|
+
this.circuit.lastFailure = Date.now();
|
|
145
|
+
if (this.circuit.failures >= this.circuitThreshold) {
|
|
146
|
+
this.circuit.isOpen = true;
|
|
147
|
+
metrics.inc("llm_circuit_opens");
|
|
148
|
+
this.emit("llm-circuit-open", {
|
|
149
|
+
provider: this.config.provider,
|
|
150
|
+
failures: this.circuit.failures
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
recordSuccess() {
|
|
155
|
+
this.circuit.failures = 0;
|
|
156
|
+
this.circuit.isOpen = false;
|
|
157
|
+
}
|
|
158
|
+
/** Generate text, returning the string content */
|
|
159
|
+
async generate(prompt) {
|
|
160
|
+
const cached = this.cache.get(prompt);
|
|
161
|
+
if (cached !== null) {
|
|
162
|
+
metrics.inc("llm_cache_hits");
|
|
163
|
+
this.emit("llm-cache-hit", { promptLength: prompt.length });
|
|
164
|
+
return cached;
|
|
165
|
+
}
|
|
166
|
+
metrics.inc("llm_calls");
|
|
167
|
+
if (!this.isCircuitOpen()) {
|
|
168
|
+
try {
|
|
169
|
+
return await this.generateWithRetry(
|
|
170
|
+
this.config.provider,
|
|
171
|
+
this.config.apiKey,
|
|
172
|
+
this.config.model,
|
|
173
|
+
prompt
|
|
174
|
+
);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
this.recordFailure();
|
|
177
|
+
this.emit("llm-primary-failed", {
|
|
178
|
+
provider: this.config.provider,
|
|
179
|
+
error: e instanceof Error ? e.message : String(e)
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (this.config.fallbackProvider && this.config.fallbackApiKey) {
|
|
184
|
+
try {
|
|
185
|
+
metrics.inc("llm_fallbacks");
|
|
186
|
+
this.emit("llm-fallback", {
|
|
187
|
+
from: this.config.provider,
|
|
188
|
+
to: this.config.fallbackProvider
|
|
189
|
+
});
|
|
190
|
+
const result = await this.generateWithRetry(
|
|
191
|
+
this.config.fallbackProvider,
|
|
192
|
+
this.config.fallbackApiKey,
|
|
193
|
+
this.config.fallbackModel,
|
|
194
|
+
prompt
|
|
195
|
+
);
|
|
196
|
+
this.cache.set(prompt, result);
|
|
197
|
+
return result;
|
|
198
|
+
} catch (e) {
|
|
199
|
+
throw new BrainError(
|
|
200
|
+
`Both LLM providers failed. Primary: ${this.config.provider}, Fallback: ${this.config.fallbackProvider}`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
throw new BrainError(
|
|
205
|
+
`LLM provider ${this.config.provider} failed and no fallback is configured`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
async generateWithRetry(provider, apiKey, model, prompt) {
|
|
209
|
+
const adapter = this.createAdapter(provider, apiKey, model);
|
|
210
|
+
let lastError;
|
|
211
|
+
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
|
212
|
+
try {
|
|
213
|
+
const result = await adapter.generate(prompt);
|
|
214
|
+
const text = result.content;
|
|
215
|
+
this.recordSuccess();
|
|
216
|
+
this.cache.set(prompt, text);
|
|
217
|
+
return text;
|
|
218
|
+
} catch (e) {
|
|
219
|
+
lastError = e;
|
|
220
|
+
if (attempt < this.maxRetries) {
|
|
221
|
+
const delayMs = Math.pow(2, attempt - 1) * 1e3;
|
|
222
|
+
metrics.inc("llm_retries");
|
|
223
|
+
this.emit("llm-retry", {
|
|
224
|
+
provider,
|
|
225
|
+
attempt,
|
|
226
|
+
maxRetries: this.maxRetries,
|
|
227
|
+
delayMs,
|
|
228
|
+
error: e instanceof Error ? e.message : String(e)
|
|
229
|
+
});
|
|
230
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
throw lastError;
|
|
235
|
+
}
|
|
236
|
+
getCircuitState() {
|
|
237
|
+
return {
|
|
238
|
+
isOpen: this.isCircuitOpen(),
|
|
239
|
+
failures: this.circuit.failures
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
getCacheStats() {
|
|
243
|
+
return this.cache.stats();
|
|
244
|
+
}
|
|
245
|
+
clearCache() {
|
|
246
|
+
this.cache.clear();
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
export {
|
|
250
|
+
ResilientLLM
|
|
251
|
+
};
|
|
252
|
+
//# sourceMappingURL=llm-resilience.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../agent/brain/llm-resilience.ts","../../../agent/errors.ts","../../../agent/brain/llm-cache.ts","../../../agent/metrics.ts"],"sourcesContent":["import { OpenAIAdapter } from '@orka-js/openai';\nimport { AnthropicAdapter } from '@orka-js/anthropic';\nimport type { LLMAdapter } from '@orka-js/core';\nimport { EventEmitter } from 'events';\nimport { BrainError } from '../errors.js';\nimport { LLMCache } from './llm-cache.js';\nimport { metrics } from '../metrics.js';\n\ninterface ResilientLLMConfig {\n provider: string;\n apiKey: string;\n model?: string;\n fallbackProvider?: string;\n fallbackApiKey?: string;\n fallbackModel?: string;\n maxRetries?: number;\n circuitThreshold?: number;\n circuitResetMs?: number;\n cacheTtlMs?: number;\n cacheMaxSize?: number;\n}\n\ninterface CircuitState {\n failures: number;\n lastFailure: number;\n isOpen: boolean;\n}\n\nexport class ResilientLLM extends EventEmitter {\n private config: ResilientLLMConfig;\n private circuit: CircuitState = { failures: 0, lastFailure: 0, isOpen: false };\n private maxRetries: number;\n private circuitThreshold: number;\n private circuitResetMs: number;\n private cache: LLMCache;\n\n constructor(config: ResilientLLMConfig) {\n super();\n this.config = config;\n this.maxRetries = config.maxRetries ?? 3;\n this.circuitThreshold = config.circuitThreshold ?? 5;\n this.circuitResetMs = config.circuitResetMs ?? 30000;\n this.cache = new LLMCache({\n ttlMs: config.cacheTtlMs,\n maxSize: config.cacheMaxSize,\n });\n }\n\n private createAdapter(provider: string, apiKey: string, model?: string): LLMAdapter {\n if (provider === 'anthropic') {\n return new AnthropicAdapter({\n apiKey,\n model: model || 'claude-3-5-sonnet-20241022',\n });\n }\n return new OpenAIAdapter({\n apiKey,\n model: model || 'gpt-4',\n });\n }\n\n private isCircuitOpen(): boolean {\n if (!this.circuit.isOpen) return false;\n // Check if enough time has passed to half-open\n if (Date.now() - this.circuit.lastFailure >= this.circuitResetMs) {\n this.circuit.isOpen = false;\n this.emit('llm-circuit-half-open', { provider: this.config.provider });\n return false;\n }\n return true;\n }\n\n private recordFailure() {\n this.circuit.failures++;\n this.circuit.lastFailure = Date.now();\n if (this.circuit.failures >= this.circuitThreshold) {\n this.circuit.isOpen = true;\n metrics.inc('llm_circuit_opens');\n this.emit('llm-circuit-open', {\n provider: this.config.provider,\n failures: this.circuit.failures,\n });\n }\n }\n\n private recordSuccess() {\n this.circuit.failures = 0;\n this.circuit.isOpen = false;\n }\n\n /** Generate text, returning the string content */\n async generate(prompt: string): Promise<string> {\n // Check cache first\n const cached = this.cache.get(prompt);\n if (cached !== null) {\n metrics.inc('llm_cache_hits');\n this.emit('llm-cache-hit', { promptLength: prompt.length });\n return cached;\n }\n\n metrics.inc('llm_calls');\n\n // Try primary provider (unless circuit is open)\n if (!this.isCircuitOpen()) {\n try {\n return await this.generateWithRetry(\n this.config.provider,\n this.config.apiKey,\n this.config.model,\n prompt,\n );\n } catch (e) {\n this.recordFailure();\n this.emit('llm-primary-failed', {\n provider: this.config.provider,\n error: e instanceof Error ? e.message : String(e),\n });\n }\n }\n\n // Try fallback provider\n if (this.config.fallbackProvider && this.config.fallbackApiKey) {\n try {\n metrics.inc('llm_fallbacks');\n this.emit('llm-fallback', {\n from: this.config.provider,\n to: this.config.fallbackProvider,\n });\n const result = await this.generateWithRetry(\n this.config.fallbackProvider,\n this.config.fallbackApiKey,\n this.config.fallbackModel,\n prompt,\n );\n this.cache.set(prompt, result);\n return result;\n } catch (e) {\n throw new BrainError(\n `Both LLM providers failed. Primary: ${this.config.provider}, Fallback: ${this.config.fallbackProvider}`,\n );\n }\n }\n\n throw new BrainError(\n `LLM provider ${this.config.provider} failed and no fallback is configured`,\n );\n }\n\n private async generateWithRetry(\n provider: string,\n apiKey: string,\n model: string | undefined,\n prompt: string,\n ): Promise<string> {\n const adapter = this.createAdapter(provider, apiKey, model);\n let lastError: unknown;\n\n for (let attempt = 1; attempt <= this.maxRetries; attempt++) {\n try {\n const result = await adapter.generate(prompt);\n const text = result.content;\n this.recordSuccess();\n this.cache.set(prompt, text);\n return text;\n } catch (e) {\n lastError = e;\n if (attempt < this.maxRetries) {\n const delayMs = Math.pow(2, attempt - 1) * 1000; // 1s, 2s, 4s\n metrics.inc('llm_retries');\n this.emit('llm-retry', {\n provider,\n attempt,\n maxRetries: this.maxRetries,\n delayMs,\n error: e instanceof Error ? e.message : String(e),\n });\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n }\n }\n\n throw lastError;\n }\n\n getCircuitState(): { isOpen: boolean; failures: number } {\n return {\n isOpen: this.isCircuitOpen(),\n failures: this.circuit.failures,\n };\n }\n\n getCacheStats() {\n return this.cache.stats();\n }\n\n clearCache() {\n this.cache.clear();\n }\n}\n","// ─── Custom Error Classes for OpenQA ───\n\nexport class OpenQAError extends Error {\n readonly code: string;\n\n constructor(message: string, code: string) {\n super(message);\n this.name = 'OpenQAError';\n this.code = code;\n }\n}\n\nexport class BrowserError extends OpenQAError {\n constructor(message: string, code = 'BROWSER_ERROR') {\n super(message, code);\n this.name = 'BrowserError';\n }\n}\n\nexport class ConfigError extends OpenQAError {\n constructor(message: string, code = 'CONFIG_ERROR') {\n super(message, code);\n this.name = 'ConfigError';\n }\n}\n\nexport class DatabaseError extends OpenQAError {\n constructor(message: string, code = 'DATABASE_ERROR') {\n super(message, code);\n this.name = 'DatabaseError';\n }\n}\n\nexport class GitError extends OpenQAError {\n constructor(message: string, code = 'GIT_ERROR') {\n super(message, code);\n this.name = 'GitError';\n }\n}\n\nexport class BrainError extends OpenQAError {\n constructor(message: string, code = 'BRAIN_ERROR') {\n super(message, code);\n this.name = 'BrainError';\n }\n}\n\nexport class ProjectRunnerError extends OpenQAError {\n constructor(message: string, code = 'PROJECT_RUNNER_ERROR') {\n super(message, code);\n this.name = 'ProjectRunnerError';\n }\n}\n\nexport function isOpenQAError(e: unknown): e is OpenQAError {\n return e instanceof OpenQAError;\n}\n\nexport function toSafeMessage(e: unknown): string {\n if (e instanceof Error) return e.message;\n if (typeof e === 'string') return e;\n return 'Unknown error';\n}\n","import { createHash } from 'crypto';\n\ninterface CacheEntry {\n response: string;\n expiresAt: number;\n}\n\nexport class LLMCache {\n private store = new Map<string, CacheEntry>();\n private ttlMs: number;\n private maxSize: number;\n\n constructor(options?: { ttlMs?: number; maxSize?: number }) {\n this.ttlMs = options?.ttlMs ?? 3600000; // 1 hour\n this.maxSize = options?.maxSize ?? 500;\n }\n\n private key(prompt: string): string {\n return createHash('sha256').update(prompt).digest('hex');\n }\n\n get(prompt: string): string | null {\n const k = this.key(prompt);\n const entry = this.store.get(k);\n if (!entry) return null;\n if (Date.now() > entry.expiresAt) {\n this.store.delete(k);\n return null;\n }\n return entry.response;\n }\n\n set(prompt: string, response: string): void {\n if (this.store.size >= this.maxSize) {\n // Evict oldest entry\n const oldest = this.store.keys().next().value;\n if (oldest) this.store.delete(oldest);\n }\n this.store.set(this.key(prompt), {\n response,\n expiresAt: Date.now() + this.ttlMs,\n });\n }\n\n clear(): void {\n this.store.clear();\n }\n\n get size(): number {\n return this.store.size;\n }\n\n stats(): { size: number; ttlMs: number; maxSize: number } {\n return { size: this.store.size, ttlMs: this.ttlMs, maxSize: this.maxSize };\n }\n}\n","const startedAt = Date.now();\n\nconst counters: Record<string, number> = {\n llm_calls: 0,\n llm_cache_hits: 0,\n llm_retries: 0,\n llm_fallbacks: 0,\n llm_circuit_opens: 0,\n tests_generated: 0,\n tests_run: 0,\n tests_passed: 0,\n tests_failed: 0,\n bugs_found: 0,\n sessions_started: 0,\n ws_connections: 0,\n http_requests: 0,\n};\n\nexport const metrics = {\n inc(key: keyof typeof counters, by = 1) {\n if (key in counters) counters[key] += by;\n },\n\n snapshot() {\n const memMB = process.memoryUsage();\n return {\n uptimeSeconds: Math.floor((Date.now() - startedAt) / 1000),\n memory: {\n heapUsedMB: Math.round(memMB.heapUsed / 1024 / 1024),\n heapTotalMB: Math.round(memMB.heapTotal / 1024 / 1024),\n rssMB: Math.round(memMB.rss / 1024 / 1024),\n },\n counters: { ...counters },\n cacheHitRate: counters.llm_calls > 0\n ? Math.round((counters.llm_cache_hits / (counters.llm_calls + counters.llm_cache_hits)) * 100)\n : 0,\n };\n },\n};\n"],"mappings":"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","names":[]}
|