micro-models-agent 0.51.1 → 0.52.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/dist/cli/commands.js +162 -38
- package/dist/cli/completer.js +5 -5
- package/dist/cli/main.js +42 -54
- package/dist/cli/repl-commands.js +138 -38
- package/dist/cli/repl.js +175 -89
- package/dist/cli/run-result.js +11 -0
- package/dist/cli/security-commands.js +6 -6
- package/dist/cli/setup.js +21 -15
- package/dist/config/config.js +54 -27
- package/dist/config/defaults.js +17 -0
- package/dist/config/domains.js +179 -0
- package/dist/config/index.js +2 -1
- package/dist/config/security.js +28 -8
- package/dist/core/agent.js +162 -30
- package/dist/core/bootstrap.js +94 -17
- package/dist/core/crash-handler.js +51 -0
- package/dist/core/environment.js +199 -0
- package/dist/core/session-logger.js +60 -6
- package/dist/core/version.js +2 -0
- package/dist/i18n/en.json +120 -39
- package/dist/i18n/ru.json +91 -10
- package/dist/llm/openai-compat.js +191 -53
- package/dist/llm/orchestrator.js +5 -3
- package/dist/logger/app-logger.js +50 -4
- package/dist/main.js +1288 -635
- package/dist/modules/browser/session.js +4 -0
- package/dist/modules/certification/cli.js +58 -19
- package/dist/modules/certification/loader.js +2 -1
- package/dist/modules/certification/manifest.js +22 -14
- package/dist/modules/certification/runner.js +91 -5
- package/dist/modules/certification/scenarios.js +290 -7
- package/dist/modules/context/fact-extractor.js +6 -0
- package/dist/modules/context/manager.js +19 -2
- package/dist/modules/execution/audit-runners.js +61 -7
- package/dist/modules/execution/execution-plugin.js +219 -60
- package/dist/modules/execution/module.js +207 -18
- package/dist/modules/execution/moe-executor.js +33 -20
- package/dist/modules/execution/plan-store.js +39 -0
- package/dist/modules/execution/plan-tool.js +188 -19
- package/dist/modules/execution/planner.js +27 -23
- package/dist/modules/execution/stuck-detector.js +244 -8
- package/dist/modules/execution/tracker.js +8 -6
- package/dist/modules/execution/verifier.js +15 -2
- package/dist/modules/hallucination/detector.js +4 -0
- package/dist/modules/hallucination/factual.js +45 -5
- package/dist/modules/indexer/module.js +1 -0
- package/dist/modules/lsp/client.js +123 -12
- package/dist/modules/lsp/index.js +1 -1
- package/dist/modules/lsp/module.js +30 -2
- package/dist/modules/lsp/probe.js +11 -1
- package/dist/modules/lsp/startup-check.js +5 -2
- package/dist/modules/plugins/builtin/lint-on-write.js +144 -41
- package/dist/modules/plugins/manager.js +57 -13
- package/dist/modules/pricing/index.js +61 -0
- package/dist/modules/pricing/prices.js +129 -0
- package/dist/modules/providers/create.js +22 -0
- package/dist/modules/providers/fallback.js +79 -0
- package/dist/modules/providers/health.js +46 -0
- package/dist/modules/providers/index.js +5 -0
- package/dist/modules/providers/manager.js +161 -0
- package/dist/modules/providers/presets.js +128 -0
- package/dist/modules/providers/registry.js +22 -0
- package/dist/modules/providers/types.js +1 -0
- package/dist/modules/registry.js +1 -0
- package/dist/modules/security/command-validator.js +14 -0
- package/dist/modules/security/encryption.js +6 -6
- package/dist/modules/security/network-validator.js +17 -0
- package/dist/modules/security/path-validator.js +22 -26
- package/dist/modules/session/store.js +10 -10
- package/dist/tools/approve.js +1 -0
- package/dist/tools/attach-image.js +12 -0
- package/dist/tools/bash.js +27 -4
- package/dist/tools/browser.js +1 -0
- package/dist/tools/chunk-query.js +1 -0
- package/dist/tools/create-dir.js +1 -0
- package/dist/tools/delete-file.js +1 -0
- package/dist/tools/download-file.js +1 -0
- package/dist/tools/edit-file.js +2 -1
- package/dist/tools/enable-tools.js +1 -0
- package/dist/tools/executor.js +17 -7
- package/dist/tools/file-info.js +1 -0
- package/dist/tools/glob-tool.js +1 -0
- package/dist/tools/grep-tool.js +54 -13
- package/dist/tools/list-dir.js +1 -0
- package/dist/tools/load-skill.js +1 -0
- package/dist/tools/mcp-call.js +1 -0
- package/dist/tools/move-file.js +1 -0
- package/dist/tools/path-utils.js +51 -1
- package/dist/tools/pipeline-run.js +1 -0
- package/dist/tools/process-kill.js +11 -0
- package/dist/tools/process-list.js +1 -0
- package/dist/tools/process-log.js +9 -0
- package/dist/tools/question.js +1 -0
- package/dist/tools/read-file.js +94 -6
- package/dist/tools/recall.js +1 -0
- package/dist/tools/remember.js +1 -0
- package/dist/tools/scope-check.js +7 -5
- package/dist/tools/search-history.js +1 -0
- package/dist/tools/subagent.js +4 -4
- package/dist/tools/web-browse.js +1 -0
- package/dist/tools/web-fetch.js +27 -6
- package/dist/tools/web-search.js +70 -43
- package/dist/tools/write-file.js +1 -0
- package/dist/ui/line-editor.js +142 -23
- package/dist/ui/line-math.js +8 -4
- package/dist/ui/renderer.js +57 -7
- package/package.json +50 -48
|
@@ -65,6 +65,136 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
65
65
|
]);
|
|
66
66
|
/** Consecutive read-only tool calls that constitute a loop. */
|
|
67
67
|
const READ_ONLY_LOOP_THRESHOLD = 10;
|
|
68
|
+
/**
|
|
69
|
+
* Bash flailing: ≥BASH_FLAIL_THRESHOLD failures within the last
|
|
70
|
+
* BASH_FLAIL_WINDOW bash attempts, regardless of interleaved successes.
|
|
71
|
+
* The per-word hard-stop (bash.ts) is evaded by rotating commands
|
|
72
|
+
* (wc → cat → Get-Content → …); this window catches the rotation itself
|
|
73
|
+
* (observed in ses_mt4x3zbo: 8 failed shell tricks across ~12 iterations
|
|
74
|
+
* trying to count/merge files on Windows).
|
|
75
|
+
*/
|
|
76
|
+
const BASH_FLAIL_WINDOW = 6;
|
|
77
|
+
const BASH_FLAIL_THRESHOLD = 4;
|
|
78
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
79
|
+
/** Пути (абсолютные/относительные) + опционально :line:col. */
|
|
80
|
+
const ERROR_LOC_RE = /(?:[A-Za-z]:[\\/])?(?:[\w.@+-]+[\\/])+[\w.@+-]+\.[a-zA-Z0-9]{1,6}(?::\d+(?::\d+)?)?/g;
|
|
81
|
+
const SIGNATURE_MAX_CHARS = 120;
|
|
82
|
+
const SEARCHABLE_PATTERNS = [
|
|
83
|
+
/error TS\d+/i,
|
|
84
|
+
/ERR_[A-Z_]+/,
|
|
85
|
+
/ENOENT|EACCES|EPERM|ECONNREFUSED|ETIMEDOUT|ECONNRESET|ENOTFOUND|EADDRINUSE/i,
|
|
86
|
+
/Cannot find module|Module not found|ERR_MODULE_NOT_FOUND/i,
|
|
87
|
+
/TypeError|ReferenceError|SyntaxError|RangeError/i,
|
|
88
|
+
/is not a function|is not defined|is not a constructor/i,
|
|
89
|
+
/Cannot read properties of/i,
|
|
90
|
+
/failed to (compile|build|resolve|parse)/i,
|
|
91
|
+
/fatal error|panic/i,
|
|
92
|
+
];
|
|
93
|
+
const RUNTIME_WORDS = [
|
|
94
|
+
"bun",
|
|
95
|
+
"node",
|
|
96
|
+
"deno",
|
|
97
|
+
"npm",
|
|
98
|
+
"yarn",
|
|
99
|
+
"pnpm",
|
|
100
|
+
"pip",
|
|
101
|
+
"pip3",
|
|
102
|
+
"python",
|
|
103
|
+
"python3",
|
|
104
|
+
"tsc",
|
|
105
|
+
"tsx",
|
|
106
|
+
"npx",
|
|
107
|
+
"go",
|
|
108
|
+
"cargo",
|
|
109
|
+
"gradle",
|
|
110
|
+
"maven",
|
|
111
|
+
"make",
|
|
112
|
+
];
|
|
113
|
+
const FRAMEWORK_WORDS = [
|
|
114
|
+
"react",
|
|
115
|
+
"vue",
|
|
116
|
+
"angular",
|
|
117
|
+
"svelte",
|
|
118
|
+
"vite",
|
|
119
|
+
"next",
|
|
120
|
+
"nest",
|
|
121
|
+
"express",
|
|
122
|
+
"fastify",
|
|
123
|
+
"flask",
|
|
124
|
+
"django",
|
|
125
|
+
"spring",
|
|
126
|
+
"webpack",
|
|
127
|
+
"tailwind",
|
|
128
|
+
];
|
|
129
|
+
/**
|
|
130
|
+
* Stable identity for an error: tool + normalized first error lines (ANSI
|
|
131
|
+
* stripped, paths/locations replaced, whitespace collapsed, capped at 120
|
|
132
|
+
* chars). Two runs failing with the same root cause produce the same
|
|
133
|
+
* signature — that is what triggers the automatic web search.
|
|
134
|
+
*/
|
|
135
|
+
export function normalizeErrorSignature(toolName, output) {
|
|
136
|
+
if (!output)
|
|
137
|
+
return null;
|
|
138
|
+
const cleaned = output
|
|
139
|
+
.replace(ANSI_RE, "")
|
|
140
|
+
.replace(ERROR_LOC_RE, " <path>")
|
|
141
|
+
.split("\n")
|
|
142
|
+
.map((l) => l.trim())
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.join(" ")
|
|
145
|
+
.replace(/\s+/g, " ")
|
|
146
|
+
.trim();
|
|
147
|
+
if (!cleaned)
|
|
148
|
+
return null;
|
|
149
|
+
return `${toolName}::${cleaned.slice(0, SIGNATURE_MAX_CHARS)}`;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Gate for the automatic web search: only real, meaningful error text is
|
|
153
|
+
* worth searching for. "Command failed with exit code 1" without details is
|
|
154
|
+
* noise — searching it would just burn a query.
|
|
155
|
+
*/
|
|
156
|
+
export function isSearchableError(output) {
|
|
157
|
+
if (!output || !output.trim())
|
|
158
|
+
return false;
|
|
159
|
+
const text = output.trim();
|
|
160
|
+
return SEARCHABLE_PATTERNS.some((p) => p.test(text));
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Build the search query for a failing error: up to 3 cleaned error lines
|
|
164
|
+
* (paths stripped), prefixed with the runtime and framework detected from the
|
|
165
|
+
* step/bash context, suffixed with "how to fix". Falls back to `tool step`
|
|
166
|
+
* when the error text is empty. Capped at maxQueryChars.
|
|
167
|
+
*/
|
|
168
|
+
export function buildSearchQuery(stepDescription, lastBashCommand, lastErrorOutput, toolName, maxQueryChars) {
|
|
169
|
+
const errorText = (lastErrorOutput || "")
|
|
170
|
+
.replace(ANSI_RE, "")
|
|
171
|
+
.replace(ERROR_LOC_RE, " <path>")
|
|
172
|
+
.split("\n")
|
|
173
|
+
.map((l) => l.trim())
|
|
174
|
+
.filter(Boolean)
|
|
175
|
+
.slice(0, 3)
|
|
176
|
+
.join(" ")
|
|
177
|
+
.replace(/\s+/g, " ")
|
|
178
|
+
.trim();
|
|
179
|
+
const context = `${stepDescription} ${lastBashCommand}`.toLowerCase();
|
|
180
|
+
const runtime = RUNTIME_WORDS.find((w) => context.includes(w)) || "";
|
|
181
|
+
const framework = FRAMEWORK_WORDS.find((w) => context.includes(w)) || "";
|
|
182
|
+
let query;
|
|
183
|
+
if (errorText) {
|
|
184
|
+
query =
|
|
185
|
+
runtime && !errorText.toLowerCase().includes(runtime) ? `${runtime} ${errorText}` : errorText;
|
|
186
|
+
if (framework && !query.toLowerCase().includes(framework))
|
|
187
|
+
query = `${query} ${framework}`;
|
|
188
|
+
query = `${query} how to fix`;
|
|
189
|
+
}
|
|
190
|
+
else if (runtime || framework) {
|
|
191
|
+
query = `${[runtime, framework, "error", "how to fix"].filter(Boolean).join(" ")}`;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
query = `${toolName} ${stepDescription} how to fix`;
|
|
195
|
+
}
|
|
196
|
+
return query.slice(0, maxQueryChars);
|
|
197
|
+
}
|
|
68
198
|
export class StuckDetector {
|
|
69
199
|
threshold;
|
|
70
200
|
errorThreshold;
|
|
@@ -86,9 +216,14 @@ export class StuckDetector {
|
|
|
86
216
|
lastBashOutput = "";
|
|
87
217
|
emptyBashRunCount = 0;
|
|
88
218
|
readOnlyStreak = 0;
|
|
89
|
-
|
|
219
|
+
bashRecent = [];
|
|
220
|
+
errorSignatureCounts = new Map();
|
|
221
|
+
searchedErrorSignatures = new Set();
|
|
222
|
+
webSearchThreshold;
|
|
223
|
+
constructor(threshold = 6, errorThreshold = 3, webSearchThreshold = 5) {
|
|
90
224
|
this.threshold = threshold;
|
|
91
225
|
this.errorThreshold = errorThreshold;
|
|
226
|
+
this.webSearchThreshold = webSearchThreshold;
|
|
92
227
|
}
|
|
93
228
|
recordIteration(stepId) {
|
|
94
229
|
if (stepId === this.currentStepId) {
|
|
@@ -127,6 +262,10 @@ export class StuckDetector {
|
|
|
127
262
|
this.lastFailedTool = toolName;
|
|
128
263
|
if (output)
|
|
129
264
|
this.lastErrorOutput = output;
|
|
265
|
+
const sig = normalizeErrorSignature(toolName, output);
|
|
266
|
+
if (sig) {
|
|
267
|
+
this.errorSignatureCounts.set(sig, (this.errorSignatureCounts.get(sig) || 0) + 1);
|
|
268
|
+
}
|
|
130
269
|
}
|
|
131
270
|
getLastErrorOutput() {
|
|
132
271
|
return this.lastErrorOutput;
|
|
@@ -147,6 +286,22 @@ export class StuckDetector {
|
|
|
147
286
|
this.lastBashCommand = command;
|
|
148
287
|
this.lastBashOutput = output;
|
|
149
288
|
}
|
|
289
|
+
/** Outcome of one bash attempt (success or failure), for flailing detection. */
|
|
290
|
+
recordBashAttempt(success) {
|
|
291
|
+
this.bashRecent.push(success);
|
|
292
|
+
if (this.bashRecent.length > BASH_FLAIL_WINDOW)
|
|
293
|
+
this.bashRecent.shift();
|
|
294
|
+
}
|
|
295
|
+
/** True when most recent bash attempts failed — the model is flailing. */
|
|
296
|
+
hasBashFlailing() {
|
|
297
|
+
if (this.bashRecent.length < BASH_FLAIL_THRESHOLD)
|
|
298
|
+
return false;
|
|
299
|
+
let failures = 0;
|
|
300
|
+
for (const ok of this.bashRecent)
|
|
301
|
+
if (!ok)
|
|
302
|
+
failures++;
|
|
303
|
+
return failures >= BASH_FLAIL_THRESHOLD;
|
|
304
|
+
}
|
|
150
305
|
getLastBashCommand() {
|
|
151
306
|
return this.lastBashCommand;
|
|
152
307
|
}
|
|
@@ -156,6 +311,32 @@ export class StuckDetector {
|
|
|
156
311
|
getLastFailedTool() {
|
|
157
312
|
return this.lastFailedTool;
|
|
158
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* First error signature whose repetition count reached the web-search
|
|
316
|
+
* threshold and that has NOT been searched yet (or was unmarked for a
|
|
317
|
+
* retry). This is what triggers the automatic web search — not iterations
|
|
318
|
+
* and not the per-tool error count.
|
|
319
|
+
*/
|
|
320
|
+
getRepeatedErrorSignature() {
|
|
321
|
+
for (const [sig, count] of this.errorSignatureCounts) {
|
|
322
|
+
if (count >= this.webSearchThreshold && !this.searchedErrorSignatures.has(sig)) {
|
|
323
|
+
return sig;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
getErrorSignatureCount(sig) {
|
|
329
|
+
return this.errorSignatureCounts.get(sig) || 0;
|
|
330
|
+
}
|
|
331
|
+
markErrorSearched(sig) {
|
|
332
|
+
this.searchedErrorSignatures.add(sig);
|
|
333
|
+
}
|
|
334
|
+
unmarkErrorSearched(sig) {
|
|
335
|
+
this.searchedErrorSignatures.delete(sig);
|
|
336
|
+
}
|
|
337
|
+
hasErrorSearched(sig) {
|
|
338
|
+
return this.searchedErrorSignatures.has(sig);
|
|
339
|
+
}
|
|
159
340
|
getIterationsOnCurrentStep() {
|
|
160
341
|
return this.iterationsOnCurrentStep;
|
|
161
342
|
}
|
|
@@ -163,6 +344,23 @@ export class StuckDetector {
|
|
|
163
344
|
this.consecutiveFailures = 0;
|
|
164
345
|
this.lastFailedTool = "";
|
|
165
346
|
this.escalationCount = 0;
|
|
347
|
+
// Decay per-tool error counters on productive calls: a tool that failed
|
|
348
|
+
// early must not keep the "Tool X failed N times" warning alive forever
|
|
349
|
+
// (ses_mt4fn58c — bash failed 3x, warning re-logged after 30 successful
|
|
350
|
+
// writes). Fresh failures rebuild the count.
|
|
351
|
+
for (const [tool, count] of this.toolErrors) {
|
|
352
|
+
if (count <= 1)
|
|
353
|
+
this.toolErrors.delete(tool);
|
|
354
|
+
else
|
|
355
|
+
this.toolErrors.set(tool, count - 1);
|
|
356
|
+
}
|
|
357
|
+
// Decay iteration counter on productive tool calls — reading new files,
|
|
358
|
+
// writing, editing all count as progress. Prevents false-positive stuck
|
|
359
|
+
// warnings during legitimate file exploration (e.g. reading 12 files in
|
|
360
|
+
// an analysis step).
|
|
361
|
+
if (this.iterationsOnCurrentStep > 0) {
|
|
362
|
+
this.iterationsOnCurrentStep = Math.max(0, this.iterationsOnCurrentStep - 1);
|
|
363
|
+
}
|
|
166
364
|
}
|
|
167
365
|
setCurrentStep(stepId, description) {
|
|
168
366
|
if (stepId !== this.currentStepId) {
|
|
@@ -231,16 +429,16 @@ export class StuckDetector {
|
|
|
231
429
|
const hints = [];
|
|
232
430
|
const desc = this.currentStepDescription.toLowerCase();
|
|
233
431
|
if (desc.includes("install") || desc.includes("npm") || desc.includes("pip")) {
|
|
234
|
-
hints.push("
|
|
432
|
+
hints.push(t("exec.hint_deps"));
|
|
235
433
|
}
|
|
236
434
|
if (desc.includes("test") || desc.includes("spec")) {
|
|
237
|
-
hints.push("
|
|
435
|
+
hints.push(t("exec.hint_test"));
|
|
238
436
|
}
|
|
239
437
|
if (desc.includes("build") || desc.includes("compile")) {
|
|
240
|
-
hints.push("
|
|
438
|
+
hints.push(t("exec.hint_build"));
|
|
241
439
|
}
|
|
242
440
|
if (desc.includes("deploy") || desc.includes("publish")) {
|
|
243
|
-
hints.push("
|
|
441
|
+
hints.push(t("exec.hint_deploy"));
|
|
244
442
|
}
|
|
245
443
|
// State-based bullets. The recovery message (getRecoveryMessage) already
|
|
246
444
|
// reports the PRIMARY reason (priority: stuck > tool-errors > consecutive
|
|
@@ -249,13 +447,13 @@ export class StuckDetector {
|
|
|
249
447
|
// bullet when the recovery message is NOT already covering that state.
|
|
250
448
|
const primary = this.getPrimaryReason();
|
|
251
449
|
if (this.hasRepetitiveToolCalls() && primary !== "repetitive") {
|
|
252
|
-
hints.push("
|
|
450
|
+
hints.push(t("exec.hint_repetitive"));
|
|
253
451
|
}
|
|
254
452
|
if (this.hasConsecutiveFailures() && primary !== "consecutive") {
|
|
255
|
-
hints.push("
|
|
453
|
+
hints.push(t("exec.hint_consecutive"));
|
|
256
454
|
}
|
|
257
455
|
if (this.hasReadOnlyLoop() && primary !== "read-only") {
|
|
258
|
-
hints.push("
|
|
456
|
+
hints.push(t("exec.hint_read_only"));
|
|
259
457
|
}
|
|
260
458
|
return hints;
|
|
261
459
|
}
|
|
@@ -379,6 +577,26 @@ export class StuckDetector {
|
|
|
379
577
|
count: this.repetitionThreshold,
|
|
380
578
|
});
|
|
381
579
|
}
|
|
580
|
+
/**
|
|
581
|
+
* Stable warning identity for anti-spam dedup: the same key re-logged every
|
|
582
|
+
* iteration is spam, a NEW key (or a long gap) deserves a re-warn.
|
|
583
|
+
*/
|
|
584
|
+
getWarningKey() {
|
|
585
|
+
if (this.isStuck())
|
|
586
|
+
return "stuck";
|
|
587
|
+
const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
|
|
588
|
+
if (errorTool)
|
|
589
|
+
return `tool-errors:${errorTool[0]}`;
|
|
590
|
+
if (this.hasBashFlailing())
|
|
591
|
+
return "bash-flailing";
|
|
592
|
+
if (this.hasConsecutiveFailures())
|
|
593
|
+
return "consecutive";
|
|
594
|
+
if (this.hasRepetitiveToolCalls())
|
|
595
|
+
return "repetitive";
|
|
596
|
+
if (this.hasReadOnlyLoop())
|
|
597
|
+
return "read-only";
|
|
598
|
+
return "";
|
|
599
|
+
}
|
|
382
600
|
getStuckReason() {
|
|
383
601
|
if (this.isStuck()) {
|
|
384
602
|
return t("exec.stuck", {
|
|
@@ -421,6 +639,13 @@ export class StuckDetector {
|
|
|
421
639
|
count: this.consecutiveFailures,
|
|
422
640
|
});
|
|
423
641
|
}
|
|
642
|
+
if (reason === "bash-flailing") {
|
|
643
|
+
const fails = this.bashRecent.filter((ok) => !ok).length;
|
|
644
|
+
return t("exec.bash_flailing_recovery", {
|
|
645
|
+
fails: String(fails),
|
|
646
|
+
window: String(this.bashRecent.length),
|
|
647
|
+
});
|
|
648
|
+
}
|
|
424
649
|
if (reason === "repetitive") {
|
|
425
650
|
return this.getRepetitiveToolMessage();
|
|
426
651
|
}
|
|
@@ -444,6 +669,8 @@ export class StuckDetector {
|
|
|
444
669
|
const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
|
|
445
670
|
if (errorTool)
|
|
446
671
|
return "tool-errors";
|
|
672
|
+
if (this.hasBashFlailing())
|
|
673
|
+
return "bash-flailing";
|
|
447
674
|
if (this.hasConsecutiveFailures())
|
|
448
675
|
return "consecutive";
|
|
449
676
|
if (this.hasRepetitiveToolCalls())
|
|
@@ -465,7 +692,10 @@ export class StuckDetector {
|
|
|
465
692
|
this.lastBashCommand = "";
|
|
466
693
|
this.lastBashOutput = "";
|
|
467
694
|
this.emptyBashRunCount = 0;
|
|
695
|
+
this.bashRecent = [];
|
|
468
696
|
this.readOnlyStreak = 0;
|
|
697
|
+
this.errorSignatureCounts.clear();
|
|
698
|
+
this.searchedErrorSignatures.clear();
|
|
469
699
|
}
|
|
470
700
|
/**
|
|
471
701
|
* Reset only step-progress state, keeping the tool-call history and the
|
|
@@ -485,6 +715,9 @@ export class StuckDetector {
|
|
|
485
715
|
this.lastBashCommand = "";
|
|
486
716
|
this.lastBashOutput = "";
|
|
487
717
|
this.emptyBashRunCount = 0;
|
|
718
|
+
this.bashRecent = [];
|
|
719
|
+
this.errorSignatureCounts.clear();
|
|
720
|
+
this.searchedErrorSignatures.clear();
|
|
488
721
|
}
|
|
489
722
|
/**
|
|
490
723
|
* Reset all per-step state when moving to a new step. Prevents errors and
|
|
@@ -503,7 +736,10 @@ export class StuckDetector {
|
|
|
503
736
|
this.lastBashCommand = "";
|
|
504
737
|
this.lastBashOutput = "";
|
|
505
738
|
this.emptyBashRunCount = 0;
|
|
739
|
+
this.bashRecent = [];
|
|
506
740
|
this.readOnlyStreak = 0;
|
|
741
|
+
this.errorSignatureCounts.clear();
|
|
742
|
+
this.searchedErrorSignatures.clear();
|
|
507
743
|
// Preserve escalationCount — prevents agent from "gaming" stuck detection
|
|
508
744
|
// by marking a step done and moving to the next one
|
|
509
745
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PlanCreator } from "./planner";
|
|
1
|
+
import { PlanCreator, getPlanProgress } from "./planner";
|
|
2
2
|
export class PlanTracker {
|
|
3
3
|
plan;
|
|
4
4
|
currentStepIndex = 0;
|
|
@@ -53,13 +53,15 @@ export class PlanTracker {
|
|
|
53
53
|
return this.plan.steps.every((s) => s.status === "done" || s.status === "skipped");
|
|
54
54
|
}
|
|
55
55
|
getProgressString() {
|
|
56
|
-
const
|
|
57
|
-
const total =
|
|
58
|
-
const
|
|
56
|
+
const p = getPlanProgress(this.plan);
|
|
57
|
+
const total = p.total;
|
|
58
|
+
const settled = p.settled;
|
|
59
|
+
const pct = total > 0 ? Math.round((settled / total) * 100) : 0;
|
|
59
60
|
const barWidth = 10;
|
|
60
|
-
const filled = Math.round((
|
|
61
|
+
const filled = Math.round((settled / total) * barWidth);
|
|
61
62
|
const bar = "█".repeat(filled) + "░".repeat(barWidth - filled);
|
|
62
|
-
|
|
63
|
+
const skippedPart = p.skipped > 0 ? ` (${p.skipped} skipped)` : "";
|
|
64
|
+
return `[${this.plan.id}] ${this.plan.title} ${settled}/${total}${skippedPart} ${bar} ${pct}%`;
|
|
63
65
|
}
|
|
64
66
|
toPromptBlock() {
|
|
65
67
|
return PlanCreator.toPromptBlock(this.plan, this.currentStepIndex);
|
|
@@ -116,7 +116,7 @@ export class StepVerifier {
|
|
|
116
116
|
details,
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
|
-
async verifyStep(stepDescription) {
|
|
119
|
+
async verifyStep(stepDescription, kind) {
|
|
120
120
|
const fileMatches = extractFileLikeTokens(stripUrls(stepDescription));
|
|
121
121
|
// No named files — this step cannot be verified by file existence. Do NOT
|
|
122
122
|
// trivially pass (empty every() = true); signal noFiles so the tool guides
|
|
@@ -128,7 +128,20 @@ export class StepVerifier {
|
|
|
128
128
|
let syntaxValid = true;
|
|
129
129
|
for (const filePath of fileMatches) {
|
|
130
130
|
const resolved = findExistingFile(this.baseDir, filePath);
|
|
131
|
-
|
|
131
|
+
const exists = !!resolved;
|
|
132
|
+
if (kind === "delete") {
|
|
133
|
+
// For delete steps: file NOT existing is a pass
|
|
134
|
+
results.push({
|
|
135
|
+
passed: !exists,
|
|
136
|
+
message: exists
|
|
137
|
+
? t("verify.file_still_exists", { path: filePath })
|
|
138
|
+
: t("verify.file_not_found", { path: filePath }),
|
|
139
|
+
});
|
|
140
|
+
// Don't syntax-check a deleted file
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
// Create steps: file must exist + valid syntax
|
|
144
|
+
if (!exists) {
|
|
132
145
|
results.push({ passed: false, message: t("verify.file_not_found", { path: filePath }) });
|
|
133
146
|
continue;
|
|
134
147
|
}
|
|
@@ -19,6 +19,10 @@ export class HallucinationDetector {
|
|
|
19
19
|
getConfidenceCheck() {
|
|
20
20
|
return this.confidence;
|
|
21
21
|
}
|
|
22
|
+
/** Add files known from compaction summaries to the factual checker. */
|
|
23
|
+
addKnownFiles(files) {
|
|
24
|
+
this.factual?.addKnownFiles(files);
|
|
25
|
+
}
|
|
22
26
|
async validate(response) {
|
|
23
27
|
const confidenceResult = this.confidence.validate(response);
|
|
24
28
|
if (confidenceResult.status === "retry" || confidenceResult.status === "block") {
|
|
@@ -20,9 +20,18 @@ export class FactualCheck {
|
|
|
20
20
|
static NAME_CACHE_TTL_MS = 5_000;
|
|
21
21
|
static RESCAN_COOLDOWN_MS = 2_000;
|
|
22
22
|
static MAX_INDEXED_FILES = 20_000;
|
|
23
|
+
/** Files known from compaction summaries — survive context resets. */
|
|
24
|
+
knownFiles = new Set();
|
|
23
25
|
constructor(baseDir) {
|
|
24
26
|
this.baseDir = baseDir;
|
|
25
27
|
}
|
|
28
|
+
/** Add file paths known from compaction summaries. These are checked
|
|
29
|
+
* before the filesystem scan so bare filenames from compacted turns
|
|
30
|
+
* are recognized even when the directory is in IGNORED_DIRS. */
|
|
31
|
+
addKnownFiles(files) {
|
|
32
|
+
for (const f of files)
|
|
33
|
+
this.knownFiles.add(f);
|
|
34
|
+
}
|
|
26
35
|
validate(response) {
|
|
27
36
|
const filePaths = this.extractFilePaths(response);
|
|
28
37
|
if (filePaths.length === 0)
|
|
@@ -50,19 +59,50 @@ export class FactualCheck {
|
|
|
50
59
|
pathExists(fp) {
|
|
51
60
|
if (isAbsolute(fp))
|
|
52
61
|
return existsSync(fp);
|
|
62
|
+
// Check known files from compaction summaries first
|
|
63
|
+
if (this.knownFiles.has(fp))
|
|
64
|
+
return true;
|
|
65
|
+
for (const cand of this.dotfileVariants(fp)) {
|
|
66
|
+
if (this.knownFiles.has(cand))
|
|
67
|
+
return true;
|
|
68
|
+
if (existsSync(resolve(this.baseDir, cand)))
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
53
71
|
// "foo.json" has no directory part — treat it as a bare name that may
|
|
54
72
|
// live anywhere under baseDir (e.g. src/data/bikes.json). Resolving it
|
|
55
73
|
// against the workspace root alone produced false "unknown file" warnings.
|
|
56
74
|
if (!fp.includes("/") && !fp.includes("\\")) {
|
|
57
75
|
return this.bareNameExists(fp);
|
|
58
76
|
}
|
|
59
|
-
return
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The token regex cannot anchor a leading dot (`\b` never matches between a
|
|
81
|
+
* non-word char and `.`), so dotfiles (.prettierrc.json, .env, …) are
|
|
82
|
+
* extracted as "prettierrc.json". Try the dot-prefixed basename too before
|
|
83
|
+
* declaring the file missing. "src/config.json" also yields
|
|
84
|
+
* "src/.config.json".
|
|
85
|
+
*/
|
|
86
|
+
dotfileVariants(fp) {
|
|
87
|
+
const idx = Math.max(fp.lastIndexOf("/"), fp.lastIndexOf("\\"));
|
|
88
|
+
const base = idx >= 0 ? fp.slice(idx + 1) : fp;
|
|
89
|
+
if (base.startsWith("."))
|
|
90
|
+
return [fp];
|
|
91
|
+
return idx >= 0 ? [fp, `${fp.slice(0, idx + 1)}.${base}`] : [fp, `.${fp}`];
|
|
60
92
|
}
|
|
61
93
|
bareNameExists(name) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
94
|
+
for (const cand of this.dotfileVariants(name)) {
|
|
95
|
+
if (existsSync(resolve(this.baseDir, cand)))
|
|
96
|
+
return true;
|
|
97
|
+
if (this.indexHas(cand))
|
|
98
|
+
return true;
|
|
99
|
+
// Check known files — a bare name may match a full path from compaction
|
|
100
|
+
for (const known of this.knownFiles) {
|
|
101
|
+
if (known.endsWith(`/${cand}`) || known.endsWith(`\\${cand}`) || known === cand) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
66
106
|
// A write_file from this session may have created the file after the
|
|
67
107
|
// index was built. Do a one-off rescan (rate-limited) before warning.
|
|
68
108
|
const now = Date.now();
|