micro-models-agent 0.28.17 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands.js +3 -116
- package/dist/cli/main.js +8 -35
- package/dist/cli/repl.js +611 -110
- package/dist/cli/setup.js +12 -32
- package/dist/config/config.js +30 -46
- package/dist/config/defaults.js +1 -10
- package/dist/config/security.js +8 -15
- package/dist/core/agent-moe.js +12 -24
- package/dist/core/agent.js +47 -281
- package/dist/core/bootstrap.js +36 -52
- package/dist/core/session-logger.js +2 -35
- package/dist/i18n/en.json +15 -79
- package/dist/i18n/index.js +9 -12
- package/dist/i18n/ru.json +15 -79
- package/dist/index.js +13 -13
- package/dist/llm/openai-compat.js +10 -39
- package/dist/logger/app-logger.js +16 -83
- package/dist/main.js +625 -243
- package/dist/modules/browser/session.js +60 -108
- package/dist/modules/context/history.js +15 -0
- package/dist/modules/context/manager.js +10 -119
- package/dist/modules/execution/auditor.js +39 -33
- package/dist/modules/execution/index.js +6 -8
- package/dist/modules/execution/module.js +32 -474
- package/dist/modules/execution/moe-executor.js +40 -97
- package/dist/modules/execution/planner.js +13 -63
- package/dist/modules/execution/stuck-detector.js +39 -252
- package/dist/modules/execution/tracker.js +7 -21
- package/dist/modules/execution/verifier.js +17 -46
- package/dist/modules/hallucination/confidence.js +2 -7
- package/dist/modules/hallucination/consistency.js +42 -8
- package/dist/modules/hallucination/detector.js +21 -26
- package/dist/modules/hallucination/factual.js +150 -170
- package/dist/modules/hallucination/index.js +4 -5
- package/dist/modules/index.js +5 -5
- package/dist/modules/mcp/client.js +2 -8
- package/dist/modules/memory/store.js +0 -4
- package/dist/modules/plugins/builtin/lint-on-write.js +38 -143
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +2 -1
- package/dist/modules/processes/registry.js +35 -125
- package/dist/modules/processes/runner.js +110 -9
- package/dist/modules/security/audit-log.js +10 -30
- package/dist/modules/security/command-validator.js +16 -42
- package/dist/modules/security/content-scanner.js +8 -9
- package/dist/modules/security/network-validator.js +2 -2
- package/dist/modules/security/path-validator.js +10 -64
- package/dist/modules/security/security-policies.js +67 -221
- package/dist/modules/security/session-encryption.js +25 -42
- package/dist/modules/session/manager.js +10 -15
- package/dist/modules/session/store.js +8 -62
- package/dist/modules/skills/index.js +3 -2
- package/dist/modules/skills/matcher.js +27 -0
- package/dist/modules/skills/module.js +23 -10
- package/dist/tools/bash.js +90 -287
- package/dist/tools/create-dir.js +1 -0
- package/dist/tools/delete-file.js +1 -0
- package/dist/tools/edit-file.js +8 -10
- package/dist/tools/executor.js +7 -57
- package/dist/tools/grep-tool.js +29 -51
- package/dist/tools/index.js +40 -55
- package/dist/tools/load-skill.js +18 -14
- package/dist/tools/move-file.js +2 -3
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/read-file.js +5 -15
- package/dist/tools/search-history.js +22 -42
- package/dist/tools/subagent.js +12 -21
- package/dist/tools/web-browse.js +25 -54
- package/dist/tools/web-fetch.js +34 -60
- package/dist/tools/web-search.js +20 -39
- package/dist/tools/write-file.js +10 -13
- package/dist/ui/diff.js +16 -9
- package/dist/ui/renderer.js +6 -69
- package/package.json +1 -1
- package/dist/cli/repl-commands.js +0 -633
- package/dist/core/workspace.js +0 -76
- package/dist/logger/file-log.js +0 -151
- package/dist/modules/certification/cli.js +0 -176
- package/dist/modules/certification/fact-checker.js +0 -84
- package/dist/modules/certification/loader.js +0 -111
- package/dist/modules/certification/manifest.js +0 -50
- package/dist/modules/certification/runner.js +0 -162
- package/dist/modules/certification/scenarios.js +0 -124
- package/dist/modules/certification/types.js +0 -1
- package/dist/modules/execution/plan-coverage.js +0 -68
- package/dist/modules/execution/plan-persister.js +0 -46
- package/dist/modules/execution/plan-store.js +0 -159
- package/dist/modules/hallucination/js-identifiers.js +0 -72
- package/dist/modules/hallucination/llm-judge.js +0 -103
- package/dist/modules/lsp/client.js +0 -235
- package/dist/modules/lsp/config.js +0 -81
- package/dist/modules/lsp/index.js +0 -3
- package/dist/modules/lsp/module.js +0 -68
- package/dist/modules/lsp/types.js +0 -1
|
@@ -1,58 +1,17 @@
|
|
|
1
|
-
import { t } from
|
|
2
|
-
const TRANSIENT_PATTERNS = [
|
|
3
|
-
/timeout/i,
|
|
4
|
-
/timed out/i,
|
|
5
|
-
/ECONNREFUSED/i,
|
|
6
|
-
/ETIMEDOUT/i,
|
|
7
|
-
/ECONNRESET/i,
|
|
8
|
-
/killed/i,
|
|
9
|
-
/SIGTERM/i,
|
|
10
|
-
/SIGKILL/i,
|
|
11
|
-
/rate limit/i,
|
|
12
|
-
/429/i,
|
|
13
|
-
/503/i,
|
|
14
|
-
/502/i,
|
|
15
|
-
];
|
|
16
|
-
const BLOCKING_PATTERNS = [
|
|
17
|
-
/EACCES/i,
|
|
18
|
-
/permission denied/i,
|
|
19
|
-
/EPERM/i,
|
|
20
|
-
/ENOENT.*node_modules/i,
|
|
21
|
-
/ENOTDIR/i,
|
|
22
|
-
/invalid.*token/i,
|
|
23
|
-
/unauthorized/i,
|
|
24
|
-
/401/i,
|
|
25
|
-
/403/i,
|
|
26
|
-
];
|
|
27
|
-
export function classifyError(output) {
|
|
28
|
-
for (const p of TRANSIENT_PATTERNS) {
|
|
29
|
-
if (p.test(output))
|
|
30
|
-
return "transient";
|
|
31
|
-
}
|
|
32
|
-
for (const p of BLOCKING_PATTERNS) {
|
|
33
|
-
if (p.test(output))
|
|
34
|
-
return "blocking";
|
|
35
|
-
}
|
|
36
|
-
return "systematic";
|
|
37
|
-
}
|
|
1
|
+
import { t } from '../../i18n/index';
|
|
38
2
|
export class StuckDetector {
|
|
39
3
|
threshold;
|
|
40
4
|
errorThreshold;
|
|
41
5
|
iterationsOnCurrentStep = 0;
|
|
42
6
|
currentStepId = null;
|
|
43
7
|
toolErrors = new Map();
|
|
44
|
-
currentStepDescription =
|
|
8
|
+
currentStepDescription = '';
|
|
45
9
|
recentToolCalls = [];
|
|
46
10
|
maxRecentCalls = 10;
|
|
47
11
|
repetitionThreshold = 3;
|
|
48
12
|
consecutiveFailures = 0;
|
|
49
|
-
lastFailedTool =
|
|
50
|
-
|
|
51
|
-
escalationCount = 0;
|
|
52
|
-
escalationThreshold = 3;
|
|
53
|
-
fileRewriteCount = new Map();
|
|
54
|
-
fileRewriteThreshold = 3;
|
|
55
|
-
constructor(threshold = 6, errorThreshold = 3) {
|
|
13
|
+
lastFailedTool = '';
|
|
14
|
+
constructor(threshold = 8, errorThreshold = 3) {
|
|
56
15
|
this.threshold = threshold;
|
|
57
16
|
this.errorThreshold = errorThreshold;
|
|
58
17
|
}
|
|
@@ -61,7 +20,8 @@ export class StuckDetector {
|
|
|
61
20
|
this.iterationsOnCurrentStep++;
|
|
62
21
|
}
|
|
63
22
|
else {
|
|
64
|
-
this.
|
|
23
|
+
this.currentStepId = stepId;
|
|
24
|
+
this.iterationsOnCurrentStep = 1;
|
|
65
25
|
}
|
|
66
26
|
}
|
|
67
27
|
recordToolCall(name, args) {
|
|
@@ -71,30 +31,19 @@ export class StuckDetector {
|
|
|
71
31
|
this.recentToolCalls.shift();
|
|
72
32
|
}
|
|
73
33
|
}
|
|
74
|
-
recordToolError(toolName
|
|
34
|
+
recordToolError(toolName) {
|
|
75
35
|
this.toolErrors.set(toolName, (this.toolErrors.get(toolName) || 0) + 1);
|
|
76
36
|
this.consecutiveFailures++;
|
|
77
37
|
this.lastFailedTool = toolName;
|
|
78
|
-
if (output)
|
|
79
|
-
this.lastErrorOutput = output;
|
|
80
|
-
}
|
|
81
|
-
getLastErrorOutput() {
|
|
82
|
-
return this.lastErrorOutput;
|
|
83
|
-
}
|
|
84
|
-
getLastFailedTool() {
|
|
85
|
-
return this.lastFailedTool;
|
|
86
|
-
}
|
|
87
|
-
getIterationsOnCurrentStep() {
|
|
88
|
-
return this.iterationsOnCurrentStep;
|
|
89
38
|
}
|
|
90
39
|
recordToolSuccess() {
|
|
91
40
|
this.consecutiveFailures = 0;
|
|
92
|
-
this.lastFailedTool =
|
|
93
|
-
this.escalationCount = 0;
|
|
41
|
+
this.lastFailedTool = '';
|
|
94
42
|
}
|
|
95
43
|
setCurrentStep(stepId, description) {
|
|
96
44
|
if (stepId !== this.currentStepId) {
|
|
97
|
-
this.
|
|
45
|
+
this.currentStepId = stepId;
|
|
46
|
+
this.iterationsOnCurrentStep = 1;
|
|
98
47
|
}
|
|
99
48
|
this.currentStepDescription = description;
|
|
100
49
|
}
|
|
@@ -105,37 +54,22 @@ export class StuckDetector {
|
|
|
105
54
|
if (toolName) {
|
|
106
55
|
return (this.toolErrors.get(toolName) || 0) >= this.errorThreshold;
|
|
107
56
|
}
|
|
108
|
-
return Array.from(this.toolErrors.values()).some(
|
|
57
|
+
return Array.from(this.toolErrors.values()).some(c => c >= this.errorThreshold);
|
|
109
58
|
}
|
|
110
59
|
hasRepetitiveToolCalls() {
|
|
111
60
|
if (this.recentToolCalls.length < this.repetitionThreshold)
|
|
112
61
|
return false;
|
|
113
62
|
const last = this.recentToolCalls[this.recentToolCalls.length - 1];
|
|
114
|
-
|
|
115
|
-
let consecutiveCount = 1;
|
|
63
|
+
let count = 1;
|
|
116
64
|
for (let i = this.recentToolCalls.length - 2; i >= 0; i--) {
|
|
117
|
-
if (this.recentToolCalls[i].name === last.name &&
|
|
118
|
-
|
|
119
|
-
consecutiveCount++;
|
|
65
|
+
if (this.recentToolCalls[i].name === last.name && this.recentToolCalls[i].argsKey === last.argsKey) {
|
|
66
|
+
count++;
|
|
120
67
|
}
|
|
121
68
|
else {
|
|
122
69
|
break;
|
|
123
70
|
}
|
|
124
71
|
}
|
|
125
|
-
|
|
126
|
-
return true;
|
|
127
|
-
// Also detect interleaved patterns (A, B, A, B, A) — count any single
|
|
128
|
-
// tool+args combo in the window and flag if it appears >= threshold times
|
|
129
|
-
const counts = new Map();
|
|
130
|
-
for (const call of this.recentToolCalls) {
|
|
131
|
-
const key = `${call.name}::${call.argsKey}`;
|
|
132
|
-
counts.set(key, (counts.get(key) || 0) + 1);
|
|
133
|
-
}
|
|
134
|
-
for (const count of counts.values()) {
|
|
135
|
-
if (count >= this.repetitionThreshold)
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
return false;
|
|
72
|
+
return count >= this.repetitionThreshold;
|
|
139
73
|
}
|
|
140
74
|
hasConsecutiveFailures() {
|
|
141
75
|
return this.consecutiveFailures >= this.errorThreshold;
|
|
@@ -143,205 +77,58 @@ export class StuckDetector {
|
|
|
143
77
|
getConsecutiveFailuresCount() {
|
|
144
78
|
return this.consecutiveFailures;
|
|
145
79
|
}
|
|
146
|
-
recordEscalation() {
|
|
147
|
-
this.escalationCount++;
|
|
148
|
-
}
|
|
149
|
-
shouldEscalate() {
|
|
150
|
-
return this.escalationCount >= this.escalationThreshold;
|
|
151
|
-
}
|
|
152
|
-
resetEscalation() {
|
|
153
|
-
this.escalationCount = 0;
|
|
154
|
-
}
|
|
155
|
-
getEscalationCount() {
|
|
156
|
-
return this.escalationCount;
|
|
157
|
-
}
|
|
158
|
-
getHints() {
|
|
159
|
-
const hints = [];
|
|
160
|
-
const desc = this.currentStepDescription.toLowerCase();
|
|
161
|
-
if (desc.includes("install") ||
|
|
162
|
-
desc.includes("npm") ||
|
|
163
|
-
desc.includes("pip")) {
|
|
164
|
-
hints.push("Check if a lock file exists (package-lock.json, poetry.lock). If missing, run the install command first.");
|
|
165
|
-
}
|
|
166
|
-
if (desc.includes("test") || desc.includes("spec")) {
|
|
167
|
-
hints.push("Make sure the source files exist and have real code before running tests.");
|
|
168
|
-
}
|
|
169
|
-
if (desc.includes("build") || desc.includes("compile")) {
|
|
170
|
-
hints.push("Check that all dependencies are installed and source files are not empty.");
|
|
171
|
-
}
|
|
172
|
-
if (desc.includes("deploy") || desc.includes("publish")) {
|
|
173
|
-
hints.push("Verify credentials and network access before deploying.");
|
|
174
|
-
}
|
|
175
|
-
if (this.hasRepetitiveToolCalls()) {
|
|
176
|
-
hints.push("You are calling the same tool repeatedly with the same arguments. Try a different approach.");
|
|
177
|
-
}
|
|
178
|
-
if (this.hasConsecutiveFailures()) {
|
|
179
|
-
hints.push("Multiple different tools are failing. Check if the environment is set up correctly.");
|
|
180
|
-
}
|
|
181
|
-
return hints;
|
|
182
|
-
}
|
|
183
|
-
getActionableHints() {
|
|
184
|
-
const hints = [];
|
|
185
|
-
const error = this.lastErrorOutput;
|
|
186
|
-
if (!error)
|
|
187
|
-
return hints;
|
|
188
|
-
// Runtime incompatibility — tool/module crashes on this Node version
|
|
189
|
-
if (/Cannot read properties of undefined|is not a function|is not a constructor/i.test(error)) {
|
|
190
|
-
if (/node_modules/.test(error)) {
|
|
191
|
-
hints.push("A dependency is incompatible with your Node.js version. Check if there is an alternative package or use a different runtime.");
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
// TypeScript syntax errors — model generated invalid code
|
|
195
|
-
if (/TS1005|TS1003|TS1109|TS1128|TS1434/i.test(error)) {
|
|
196
|
-
hints.push("TypeScript syntax error. Read the error message carefully — it tells you the exact line and column. Fix the syntax before retrying.");
|
|
197
|
-
}
|
|
198
|
-
// TypeScript type errors — model doesn't understand the types
|
|
199
|
-
if (/TS2322|TS2345|TS2769|TS7006|TS7016/i.test(error)) {
|
|
200
|
-
hints.push("TypeScript type mismatch. Check the type signature of the function/API you are using. If a package lacks type declarations, install @types/<package> or use skipLibCheck.");
|
|
201
|
-
}
|
|
202
|
-
// Module not found — missing dependency or wrong import
|
|
203
|
-
if (/Cannot find module|Module not found|ERR_MODULE_NOT_FOUND/i.test(error)) {
|
|
204
|
-
hints.push("Module not found. Run npm install or check if the import path is correct.");
|
|
205
|
-
}
|
|
206
|
-
// Permission / access errors
|
|
207
|
-
if (/EACCES|EPERM|permission denied/i.test(error)) {
|
|
208
|
-
hints.push("Permission denied. Check file permissions or run with appropriate privileges.");
|
|
209
|
-
}
|
|
210
|
-
// Network errors
|
|
211
|
-
if (/ECONNREFUSED|ETIMEDOUT|ENOTFOUND|fetch failed/i.test(error)) {
|
|
212
|
-
hints.push("Network error. Check if the server is running and accessible.");
|
|
213
|
-
}
|
|
214
|
-
// Command not found
|
|
215
|
-
if (/is not recognized|command not found|not found in path/i.test(error)) {
|
|
216
|
-
hints.push("Command not found. Check if the tool is installed and in PATH.");
|
|
217
|
-
}
|
|
218
|
-
// File rewrite loop — same file keeps failing
|
|
219
|
-
if (this.hasExcessiveRewrites()) {
|
|
220
|
-
const file = this.getExcessiveRewriteFile();
|
|
221
|
-
hints.push(`File ${file} has been rewritten ${this.getFileRewriteCount(file)} times without success. Stop rewriting and try a fundamentally different approach.`);
|
|
222
|
-
}
|
|
223
|
-
return hints;
|
|
224
|
-
}
|
|
225
|
-
getToolAlternative() {
|
|
226
|
-
const tool = this.lastFailedTool;
|
|
227
|
-
const error = this.lastErrorOutput;
|
|
228
|
-
if (!tool)
|
|
229
|
-
return null;
|
|
230
|
-
// If a tool fails with a runtime crash, suggest alternatives
|
|
231
|
-
if (/Cannot read properties of undefined|is not a function|TypeError|ReferenceError/i.test(error)) {
|
|
232
|
-
// Map of tools to their common alternatives
|
|
233
|
-
const alternatives = {
|
|
234
|
-
"ts-node": "tsx",
|
|
235
|
-
jest: "vitest",
|
|
236
|
-
mocha: "vitest",
|
|
237
|
-
webpack: "vite",
|
|
238
|
-
rollup: "vite",
|
|
239
|
-
parcel: "vite",
|
|
240
|
-
babel: "tsc",
|
|
241
|
-
eslint: "biome",
|
|
242
|
-
prettier: "biome",
|
|
243
|
-
};
|
|
244
|
-
for (const [failed, alt] of Object.entries(alternatives)) {
|
|
245
|
-
if (tool.includes(failed) || error.includes(failed)) {
|
|
246
|
-
return alt;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
recordFileRewrite(filePath) {
|
|
253
|
-
const count = (this.fileRewriteCount.get(filePath) || 0) + 1;
|
|
254
|
-
this.fileRewriteCount.set(filePath, count);
|
|
255
|
-
}
|
|
256
|
-
getFileRewriteCount(filePath) {
|
|
257
|
-
return this.fileRewriteCount.get(filePath) || 0;
|
|
258
|
-
}
|
|
259
|
-
hasExcessiveRewrites() {
|
|
260
|
-
return Array.from(this.fileRewriteCount.values()).some((c) => c >= this.fileRewriteThreshold);
|
|
261
|
-
}
|
|
262
|
-
getExcessiveRewriteFile() {
|
|
263
|
-
for (const [file, count] of this.fileRewriteCount) {
|
|
264
|
-
if (count >= this.fileRewriteThreshold)
|
|
265
|
-
return file;
|
|
266
|
-
}
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
80
|
getRepetitiveToolMessage() {
|
|
270
81
|
if (!this.hasRepetitiveToolCalls())
|
|
271
|
-
return
|
|
82
|
+
return '';
|
|
272
83
|
const last = this.recentToolCalls[this.recentToolCalls.length - 1];
|
|
273
|
-
return t(
|
|
274
|
-
tool: last.name,
|
|
275
|
-
count: this.repetitionThreshold,
|
|
276
|
-
});
|
|
84
|
+
return t('exec.repetitive_tool', { tool: last.name, count: this.repetitionThreshold });
|
|
277
85
|
}
|
|
278
86
|
getStuckReason() {
|
|
279
87
|
if (this.isStuck()) {
|
|
280
|
-
return t(
|
|
281
|
-
stepId: String(this.currentStepId ?? "?"),
|
|
282
|
-
description: this.currentStepDescription,
|
|
283
|
-
iterations: this.iterationsOnCurrentStep,
|
|
284
|
-
});
|
|
88
|
+
return t('exec.stuck', { stepId: String(this.currentStepId), description: this.currentStepDescription, iterations: this.iterationsOnCurrentStep });
|
|
285
89
|
}
|
|
286
90
|
const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
|
|
287
91
|
if (errorTool) {
|
|
288
|
-
return t(
|
|
92
|
+
return t('exec.tool_errors', { tool: errorTool[0], count: errorTool[1] });
|
|
289
93
|
}
|
|
290
|
-
return
|
|
94
|
+
return '';
|
|
291
95
|
}
|
|
292
96
|
getRecoveryMessage() {
|
|
293
97
|
if (this.isStuck()) {
|
|
294
|
-
return t(
|
|
295
|
-
iterations: this.iterationsOnCurrentStep,
|
|
296
|
-
stepId: String(this.currentStepId ?? "?"),
|
|
297
|
-
description: this.currentStepDescription,
|
|
298
|
-
});
|
|
98
|
+
return t('exec.stuck_recovery', { iterations: this.iterationsOnCurrentStep });
|
|
299
99
|
}
|
|
300
100
|
// Per-tool error recovery (more specific — e.g., "bash failed 3 times")
|
|
301
101
|
const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
|
|
302
102
|
if (errorTool) {
|
|
303
|
-
return t(
|
|
304
|
-
tool: errorTool[0],
|
|
305
|
-
count: errorTool[1],
|
|
306
|
-
});
|
|
103
|
+
return t('exec.tool_errors_recovery', { tool: errorTool[0], count: errorTool[1] });
|
|
307
104
|
}
|
|
308
105
|
// Consecutive failures from different tools (generic — e.g., "5 consecutive failures")
|
|
309
106
|
if (this.hasConsecutiveFailures()) {
|
|
310
|
-
return t(
|
|
311
|
-
count: this.consecutiveFailures,
|
|
312
|
-
});
|
|
107
|
+
return t('exec.consecutive_failures_recovery', { count: this.consecutiveFailures });
|
|
313
108
|
}
|
|
314
109
|
if (this.hasRepetitiveToolCalls()) {
|
|
315
110
|
return this.getRepetitiveToolMessage();
|
|
316
111
|
}
|
|
317
|
-
return
|
|
112
|
+
return '';
|
|
113
|
+
}
|
|
114
|
+
checkOffTrack(toolName, call) {
|
|
115
|
+
if (!this.currentStepDescription)
|
|
116
|
+
return null;
|
|
117
|
+
const toolArgStr = JSON.stringify(call.arguments).toLowerCase();
|
|
118
|
+
const pathInStep = (this.currentStepDescription.match(/\b[\w./-]+\.[a-z]+/gi) || [])
|
|
119
|
+
.map(p => p.toLowerCase());
|
|
120
|
+
const pathInCall = (toolArgStr.match(/\b[\w./-]+\.[a-z]+/gi) || [])
|
|
121
|
+
.map(p => p.toLowerCase());
|
|
122
|
+
const overlaps = pathInCall.some(p => pathInStep.some(s => p.includes(s) || s.includes(p)));
|
|
123
|
+
if (!overlaps && pathInStep.length > 0 && pathInCall.length > 0) {
|
|
124
|
+
return t('exec.off_track', { stepId: String(this.currentStepId), description: this.currentStepDescription, tool: toolName });
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
318
127
|
}
|
|
319
128
|
reset() {
|
|
320
|
-
this.currentStepId = null;
|
|
321
129
|
this.iterationsOnCurrentStep = 0;
|
|
322
130
|
this.toolErrors.clear();
|
|
323
131
|
this.consecutiveFailures = 0;
|
|
324
|
-
this.lastFailedTool =
|
|
325
|
-
this.lastErrorOutput = "";
|
|
326
|
-
this.recentToolCalls = [];
|
|
327
|
-
this.fileRewriteCount.clear();
|
|
328
|
-
this.escalationCount = 0;
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Reset all per-step state when moving to a new step. Prevents errors and
|
|
332
|
-
* tool repetitions from one step leaking into the next (false-positive
|
|
333
|
-
* stuck/recovery on the first iteration of the new step).
|
|
334
|
-
*/
|
|
335
|
-
resetStepState(stepId) {
|
|
336
|
-
this.currentStepId = stepId;
|
|
337
|
-
this.iterationsOnCurrentStep = 1;
|
|
338
|
-
this.toolErrors.clear();
|
|
339
|
-
this.consecutiveFailures = 0;
|
|
340
|
-
this.lastFailedTool = "";
|
|
341
|
-
this.lastErrorOutput = "";
|
|
342
|
-
this.recentToolCalls = [];
|
|
343
|
-
this.fileRewriteCount.clear();
|
|
344
|
-
// Preserve escalationCount — prevents agent from "gaming" stuck detection
|
|
345
|
-
// by marking a step done and moving to the next one
|
|
132
|
+
this.lastFailedTool = '';
|
|
346
133
|
}
|
|
347
134
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PlanCreator } from
|
|
1
|
+
import { PlanCreator } from './planner';
|
|
2
2
|
export class PlanTracker {
|
|
3
3
|
plan;
|
|
4
4
|
currentStepIndex = 0;
|
|
@@ -15,7 +15,7 @@ export class PlanTracker {
|
|
|
15
15
|
return this.plan.steps[this.currentStepIndex];
|
|
16
16
|
}
|
|
17
17
|
getStep(id) {
|
|
18
|
-
return this.plan.steps.find(
|
|
18
|
+
return this.plan.steps.find(s => s.id === id);
|
|
19
19
|
}
|
|
20
20
|
updateStepStatus(id, status) {
|
|
21
21
|
const step = this.getStep(id);
|
|
@@ -30,36 +30,22 @@ export class PlanTracker {
|
|
|
30
30
|
advance() {
|
|
31
31
|
if (this.currentStepIndex < this.plan.steps.length - 1) {
|
|
32
32
|
this.currentStepIndex++;
|
|
33
|
-
this.plan.steps[this.currentStepIndex].status =
|
|
33
|
+
this.plan.steps[this.currentStepIndex].status = 'in_progress';
|
|
34
34
|
return true;
|
|
35
35
|
}
|
|
36
36
|
return false;
|
|
37
37
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Advance the pointer past any done/skipped steps without mutating their
|
|
40
|
-
* status. Keeps currentStepIndex in sync when the model marks steps done via
|
|
41
|
-
* the `plan update` tool (which never calls advance()). Without this,
|
|
42
|
-
* getCurrentStep() stays on the first step forever and the stuck detector
|
|
43
|
-
* accumulates false-positive "no progress" warnings.
|
|
44
|
-
*/
|
|
45
|
-
syncCurrentStep() {
|
|
46
|
-
while (this.currentStepIndex < this.plan.steps.length - 1 &&
|
|
47
|
-
(this.plan.steps[this.currentStepIndex].status === "done" ||
|
|
48
|
-
this.plan.steps[this.currentStepIndex].status === "skipped")) {
|
|
49
|
-
this.currentStepIndex++;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
38
|
isComplete() {
|
|
53
|
-
return this.plan.steps.every(
|
|
39
|
+
return this.plan.steps.every(s => s.status === 'done' || s.status === 'skipped');
|
|
54
40
|
}
|
|
55
41
|
getProgressString() {
|
|
56
|
-
const done = this.plan.steps.filter(
|
|
42
|
+
const done = this.plan.steps.filter(s => s.status === 'done').length;
|
|
57
43
|
const total = this.plan.steps.length;
|
|
58
44
|
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
59
45
|
const barWidth = 10;
|
|
60
46
|
const filled = Math.round((done / total) * barWidth);
|
|
61
|
-
const bar =
|
|
62
|
-
return `[
|
|
47
|
+
const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
|
|
48
|
+
return `[Plan: ${this.plan.title}] ${done}/${total} ${bar} ${pct}%`;
|
|
63
49
|
}
|
|
64
50
|
toPromptBlock() {
|
|
65
51
|
return PlanCreator.toPromptBlock(this.plan, this.currentStepIndex);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
2
|
import { resolve, extname } from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
4
|
import { t } from '../../i18n/index';
|
|
5
5
|
import { validateExpertConfig } from '../../config/experts';
|
|
6
6
|
export class StepVerifier {
|
|
@@ -18,7 +18,12 @@ export class StepVerifier {
|
|
|
18
18
|
}
|
|
19
19
|
async runScript(scriptName) {
|
|
20
20
|
try {
|
|
21
|
-
|
|
21
|
+
execSync(`bun run ${scriptName}`, {
|
|
22
|
+
cwd: this.baseDir,
|
|
23
|
+
encoding: 'utf-8',
|
|
24
|
+
timeout: 60_000,
|
|
25
|
+
stdio: 'pipe',
|
|
26
|
+
});
|
|
22
27
|
return { passed: true, message: t('verify.script_passed', { script: scriptName }) };
|
|
23
28
|
}
|
|
24
29
|
catch (e) {
|
|
@@ -31,7 +36,12 @@ export class StepVerifier {
|
|
|
31
36
|
return { passed: true, message: 'No tsconfig.json found — skipping type check' };
|
|
32
37
|
}
|
|
33
38
|
try {
|
|
34
|
-
|
|
39
|
+
execSync('npx tsc --noEmit', {
|
|
40
|
+
cwd: this.baseDir,
|
|
41
|
+
encoding: 'utf-8',
|
|
42
|
+
timeout: 60_000,
|
|
43
|
+
stdio: 'pipe',
|
|
44
|
+
});
|
|
35
45
|
return { passed: true, message: 'TypeScript type check passed' };
|
|
36
46
|
}
|
|
37
47
|
catch (e) {
|
|
@@ -95,7 +105,7 @@ export class StepVerifier {
|
|
|
95
105
|
results.push(result);
|
|
96
106
|
if (result.passed) {
|
|
97
107
|
const fullPath = resolve(this.baseDir, filePath);
|
|
98
|
-
if (!
|
|
108
|
+
if (!this.validateSyntax(fullPath)) {
|
|
99
109
|
syntaxValid = false;
|
|
100
110
|
results.push({ passed: false, message: t('verify.syntax_error', { path: filePath }) });
|
|
101
111
|
}
|
|
@@ -107,11 +117,11 @@ export class StepVerifier {
|
|
|
107
117
|
failed: results.filter(r => !r.passed),
|
|
108
118
|
};
|
|
109
119
|
}
|
|
110
|
-
|
|
120
|
+
validateSyntax(filePath) {
|
|
111
121
|
const ext = extname(filePath);
|
|
112
122
|
if (ext === '.ts' || ext === '.tsx') {
|
|
113
123
|
try {
|
|
114
|
-
|
|
124
|
+
execSync(`npx tsc --noEmit --skipLibCheck ${filePath}`, { stdio: 'pipe', timeout: 10000 });
|
|
115
125
|
return true;
|
|
116
126
|
}
|
|
117
127
|
catch (err) {
|
|
@@ -127,7 +137,7 @@ export class StepVerifier {
|
|
|
127
137
|
}
|
|
128
138
|
if (ext === '.js' || ext === '.jsx') {
|
|
129
139
|
try {
|
|
130
|
-
|
|
140
|
+
execSync(`node --check ${filePath}`, { stdio: 'pipe', timeout: 5000 });
|
|
131
141
|
return true;
|
|
132
142
|
}
|
|
133
143
|
catch {
|
|
@@ -136,43 +146,4 @@ export class StepVerifier {
|
|
|
136
146
|
}
|
|
137
147
|
return true;
|
|
138
148
|
}
|
|
139
|
-
/**
|
|
140
|
-
* Run a command asynchronously (non-blocking event loop) and resolve when it
|
|
141
|
-
* exits. Rejects with the captured stderr/stdout on non-zero exit or timeout.
|
|
142
|
-
*/
|
|
143
|
-
runAsync(command, cwd, timeoutMs) {
|
|
144
|
-
return new Promise((resolve, reject) => {
|
|
145
|
-
const child = spawn(command, {
|
|
146
|
-
cwd,
|
|
147
|
-
shell: true,
|
|
148
|
-
windowsHide: true,
|
|
149
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
150
|
-
});
|
|
151
|
-
let stdout = '';
|
|
152
|
-
let stderr = '';
|
|
153
|
-
child.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
154
|
-
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
155
|
-
const timer = setTimeout(() => {
|
|
156
|
-
child.kill();
|
|
157
|
-
reject(new Error(`Command timed out after ${timeoutMs}ms`));
|
|
158
|
-
}, timeoutMs);
|
|
159
|
-
child.on('error', (err) => {
|
|
160
|
-
clearTimeout(timer);
|
|
161
|
-
reject(err);
|
|
162
|
-
});
|
|
163
|
-
child.on('close', (code) => {
|
|
164
|
-
clearTimeout(timer);
|
|
165
|
-
if (code === 0) {
|
|
166
|
-
resolve({ stdout, stderr });
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
const err = new Error(`Command failed with exit code ${code}`);
|
|
170
|
-
err.stdout = stdout;
|
|
171
|
-
err.stderr = stderr;
|
|
172
|
-
err.status = code;
|
|
173
|
-
reject(err);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
149
|
}
|
|
@@ -30,19 +30,14 @@ export class ConfidenceCheck {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
// Language-agnostic: very low word diversity (same words repeated)
|
|
33
|
-
const words = response
|
|
34
|
-
.toLowerCase()
|
|
35
|
-
.split(/\s+/)
|
|
36
|
-
.filter((w) => w.length > 2);
|
|
33
|
+
const words = response.toLowerCase().split(/\s+/).filter(w => w.length > 2);
|
|
37
34
|
if (words.length >= 10) {
|
|
38
35
|
const unique = new Set(words);
|
|
39
36
|
const diversity = unique.size / words.length;
|
|
40
37
|
if (diversity < 0.25) {
|
|
41
38
|
return {
|
|
42
39
|
status: "warn",
|
|
43
|
-
reason: t("hall.repetitive", {
|
|
44
|
-
pct: Math.round((1 - diversity) * 100),
|
|
45
|
-
}),
|
|
40
|
+
reason: t("hall.repetitive", { pct: Math.round((1 - diversity) * 100) }),
|
|
46
41
|
};
|
|
47
42
|
}
|
|
48
43
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t } from '../../i18n/index';
|
|
1
2
|
export class ConsistencyCheck {
|
|
2
3
|
decisions = [];
|
|
3
4
|
createdFiles = new Set();
|
|
@@ -14,13 +15,46 @@ export class ConsistencyCheck {
|
|
|
14
15
|
getCreatedFiles() {
|
|
15
16
|
return Array.from(this.createdFiles);
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
validate(response) {
|
|
19
|
+
const lower = response.toLowerCase();
|
|
20
|
+
for (const d of this.decisions) {
|
|
21
|
+
const decisionWords = d.decision.toLowerCase().split(/\s+/).filter(w => w.length > 3);
|
|
22
|
+
const contradicts = decisionWords.some(word => {
|
|
23
|
+
// English patterns
|
|
24
|
+
if (lower.includes(`instead of ${word}`))
|
|
25
|
+
return true;
|
|
26
|
+
if (lower.includes(`not ${word}`))
|
|
27
|
+
return true;
|
|
28
|
+
if (lower.includes(`replacing ${word} with`))
|
|
29
|
+
return true;
|
|
30
|
+
if (lower.includes(`switching to`))
|
|
31
|
+
return true;
|
|
32
|
+
if (lower.includes(`changing from ${word}`))
|
|
33
|
+
return true;
|
|
34
|
+
if (lower.includes(`abandoning ${word}`))
|
|
35
|
+
return true;
|
|
36
|
+
// Russian patterns
|
|
37
|
+
if (lower.includes(`вместо ${word}`))
|
|
38
|
+
return true;
|
|
39
|
+
if (lower.includes(`заменяя ${word}`))
|
|
40
|
+
return true;
|
|
41
|
+
if (lower.includes(`заменяем ${word}`))
|
|
42
|
+
return true;
|
|
43
|
+
if (lower.includes(`переключаемся на`))
|
|
44
|
+
return true;
|
|
45
|
+
if (lower.includes(`от ${word} к`))
|
|
46
|
+
return true;
|
|
47
|
+
if (lower.includes(`отказываемся от ${word}`))
|
|
48
|
+
return true;
|
|
49
|
+
return false;
|
|
50
|
+
});
|
|
51
|
+
if (contradicts) {
|
|
52
|
+
return {
|
|
53
|
+
status: 'warn',
|
|
54
|
+
reason: t('hall.contradiction', { decision: d.decision, location: d.location }),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { status: 'pass' };
|
|
25
59
|
}
|
|
26
60
|
}
|