micro-models-agent 0.40.1 → 0.41.2
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/bin/mma.mjs +41 -41
- package/dist/cli/commands.js +9 -19
- package/dist/cli/completer.js +36 -37
- package/dist/cli/index.js +2 -2
- package/dist/cli/main.js +48 -23
- package/dist/cli/plugin-commands.js +36 -0
- package/dist/cli/repl-commands.js +40 -12
- package/dist/cli/repl.js +217 -87
- package/dist/cli/run-result.js +22 -0
- package/dist/cli/security-commands.js +5 -7
- package/dist/cli/setup.js +8 -26
- package/dist/config/config.js +52 -5
- package/dist/config/defaults.js +29 -5
- package/dist/config/experts.js +1 -1
- package/dist/config/index.js +3 -3
- package/dist/config/security.js +3 -10
- package/dist/core/agent-moe.js +2 -10
- package/dist/core/agent.js +273 -82
- package/dist/core/bootstrap.js +80 -13
- package/dist/core/index.js +2 -2
- package/dist/core/prompt-builder.js +23 -2
- package/dist/core/session-logger.js +46 -4
- package/dist/core/version.js +24 -0
- package/dist/i18n/en.json +75 -2
- package/dist/i18n/ru.json +74 -1
- package/dist/index.js +1 -1
- package/dist/llm/image-utils.js +4 -5
- package/dist/llm/index.js +4 -4
- package/dist/llm/model-loader.js +6 -6
- package/dist/llm/openai-compat.js +40 -34
- package/dist/llm/orchestrator.js +33 -29
- package/dist/llm/response.js +9 -9
- package/dist/logger/app-logger.js +1 -1
- package/dist/logger/index.js +1 -1
- package/dist/main.js +2489 -2186
- package/dist/migration/backup.js +13 -13
- package/dist/migration/detect.js +11 -11
- package/dist/migration/index.js +2 -2
- package/dist/modules/artifacts/store.js +61 -0
- package/dist/modules/browser/actions.js +34 -4
- package/dist/modules/browser/bridge-client.js +199 -0
- package/dist/modules/browser/bridge-path.js +10 -0
- package/dist/modules/browser/bridge-server.mjs +202 -202
- package/dist/modules/browser/cookie-store.js +6 -6
- package/dist/modules/browser/driver.js +136 -0
- package/dist/modules/browser/index.js +7 -5
- package/dist/modules/browser/module.js +8 -7
- package/dist/modules/browser/session.js +87 -84
- package/dist/modules/browser/snapshot.js +92 -58
- package/dist/modules/browser/types.js +4 -1
- package/dist/modules/certification/cli.js +2 -4
- package/dist/modules/certification/fact-checker.js +1 -3
- package/dist/modules/certification/loader.js +3 -9
- package/dist/modules/certification/runner.js +1 -4
- package/dist/modules/context/chunk-query.js +100 -0
- package/dist/modules/context/fact-extractor.js +162 -0
- package/dist/modules/context/history.js +15 -0
- package/dist/modules/context/index.js +1 -1
- package/dist/modules/context/manager.js +160 -86
- package/dist/modules/execution/audit-runners.js +152 -0
- package/dist/modules/execution/auditor.js +177 -25
- package/dist/modules/execution/execution-plugin.js +272 -0
- package/dist/modules/execution/module.js +201 -544
- package/dist/modules/execution/moe-executor.js +25 -0
- package/dist/modules/execution/plan-store.js +1 -3
- package/dist/modules/execution/plan-tool.js +508 -0
- package/dist/modules/execution/plan-validator.js +10 -10
- package/dist/modules/execution/planner.js +6 -1
- package/dist/modules/execution/stuck-detector.js +173 -10
- package/dist/modules/execution/verifier.js +86 -42
- package/dist/modules/execution/windows-commands.js +41 -0
- package/dist/modules/hallucination/confidence.js +8 -1
- package/dist/modules/hallucination/detector.js +2 -5
- package/dist/modules/hallucination/factual.js +3 -64
- package/dist/modules/hallucination/index.js +1 -1
- package/dist/modules/hallucination/js-identifiers.js +190 -0
- package/dist/modules/hallucination/llm-judge.js +1 -3
- package/dist/modules/indexer/cache.js +9 -7
- package/dist/modules/indexer/index.js +3 -3
- package/dist/modules/indexer/module.js +95 -42
- package/dist/modules/indexer/project-profile.js +183 -0
- package/dist/modules/indexer/walker.js +17 -17
- package/dist/modules/lsp/check-tool.js +58 -0
- package/dist/modules/lsp/client.js +74 -31
- package/dist/modules/lsp/command.js +60 -0
- package/dist/modules/lsp/config.js +87 -33
- package/dist/modules/lsp/index.js +3 -3
- package/dist/modules/lsp/module.js +185 -21
- package/dist/modules/lsp/probe.js +76 -0
- package/dist/modules/lsp/project-root.js +32 -0
- package/dist/modules/lsp/startup-check.js +141 -0
- package/dist/modules/mcp/module.js +2 -6
- package/dist/modules/memory/index.js +1 -1
- package/dist/modules/memory/module.js +71 -23
- package/dist/modules/memory/search.js +11 -9
- package/dist/modules/memory/store.js +13 -13
- package/dist/modules/pipelines/engine.js +10 -10
- package/dist/modules/pipelines/index.js +3 -3
- package/dist/modules/pipelines/parser.js +17 -14
- package/dist/modules/pipelines/template.js +1 -1
- package/dist/modules/plugins/builtin/lint-on-write.js +21 -16
- package/dist/modules/plugins/builtin/notify.js +3 -2
- package/dist/modules/plugins/index.js +1 -1
- package/dist/modules/plugins/loader.js +59 -17
- package/dist/modules/plugins/manager.js +73 -17
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +1 -1
- package/dist/modules/processes/registry.js +135 -46
- package/dist/modules/registry.js +4 -2
- package/dist/modules/security/audit-notifier.js +39 -39
- package/dist/modules/security/command-validator.js +2 -8
- package/dist/modules/security/data-sanitizer.js +1 -9
- package/dist/modules/security/encryption.js +58 -56
- package/dist/modules/security/network-validator.js +1 -9
- package/dist/modules/security/path-validator.js +1 -3
- package/dist/modules/security/security-policies.js +3 -19
- package/dist/modules/security/session-encryption.js +1 -1
- package/dist/modules/security/session-isolation.js +8 -8
- package/dist/modules/session/index.js +3 -3
- package/dist/modules/session/module.js +5 -5
- package/dist/modules/session/store.js +3 -9
- package/dist/modules/skills/matcher.js +27 -0
- package/dist/modules/skills/module.js +1 -2
- package/dist/modules/updater/checker.js +70 -6
- package/dist/modules/updater/index.js +2 -1
- package/dist/modules/updater/module.js +116 -0
- package/dist/modules/user-profile/compressor.js +2 -2
- package/dist/modules/user-profile/index.js +1 -1
- package/dist/modules/user-profile/profile.js +9 -9
- package/dist/tools/attach-image.js +1 -1
- package/dist/tools/bash.js +178 -19
- package/dist/tools/browser.js +46 -29
- package/dist/tools/chunk-query.js +99 -0
- package/dist/tools/download-file.js +116 -0
- package/dist/tools/enable-tools.js +58 -0
- package/dist/tools/executor.js +4 -5
- package/dist/tools/file-info.js +13 -12
- package/dist/tools/filter-tools.js +9 -2
- package/dist/tools/glob-tool.js +11 -11
- package/dist/tools/grep-tool.js +1 -3
- package/dist/tools/hidden-tools-block.js +37 -0
- package/dist/tools/index.js +13 -2
- package/dist/tools/list-dir.js +18 -17
- package/dist/tools/load-skill.js +1 -3
- package/dist/tools/path-utils.js +4 -4
- package/dist/tools/pipeline-run.js +25 -25
- package/dist/tools/process-kill.js +11 -11
- package/dist/tools/process-list.js +20 -22
- package/dist/tools/process-log.js +22 -18
- package/dist/tools/question.js +1 -3
- package/dist/tools/read-file.js +10 -2
- package/dist/tools/recall.js +44 -37
- package/dist/tools/registry.js +15 -4
- package/dist/tools/remember.js +29 -29
- package/dist/tools/scope-check.js +9 -9
- package/dist/tools/subagent.js +54 -9
- package/dist/tools/user-input.js +1 -1
- package/dist/tools/web-browse.js +3 -3
- package/dist/tools/web-fetch.js +3 -3
- package/dist/tools/web-search.js +3 -3
- package/dist/tools/write-file.js +1 -3
- package/dist/ui/box.js +1 -5
- package/dist/ui/index.js +6 -6
- package/dist/ui/line-editor.js +703 -0
- package/dist/ui/line-math.js +69 -0
- package/dist/ui/md-formatter.js +33 -33
- package/dist/ui/output.js +5 -5
- package/dist/ui/plan-view.js +103 -0
- package/dist/ui/renderer.js +15 -10
- package/dist/ui/table.js +1 -1
- package/package.json +48 -48
|
@@ -35,6 +35,36 @@ export function classifyError(output) {
|
|
|
35
35
|
}
|
|
36
36
|
return "systematic";
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Tools that only observe state and never change it. A long run of only these
|
|
40
|
+
* with no write/exec in between means the model is reading/exploring without
|
|
41
|
+
* producing anything — a read-only loop that the stuck detector must surface
|
|
42
|
+
* (observed: 50+ read_file iterations with zero writes after a false-positive
|
|
43
|
+
* audit rejected a completed task).
|
|
44
|
+
*/
|
|
45
|
+
const READ_ONLY_TOOLS = new Set([
|
|
46
|
+
"read_file",
|
|
47
|
+
"glob",
|
|
48
|
+
"grep",
|
|
49
|
+
"list_dir",
|
|
50
|
+
"file_info",
|
|
51
|
+
"browser",
|
|
52
|
+
"web_search",
|
|
53
|
+
"web_fetch",
|
|
54
|
+
"web_browse",
|
|
55
|
+
"search_history",
|
|
56
|
+
"recall",
|
|
57
|
+
"project_map",
|
|
58
|
+
"chunk_query",
|
|
59
|
+
"process_list",
|
|
60
|
+
"process_log",
|
|
61
|
+
"plan",
|
|
62
|
+
"todo",
|
|
63
|
+
"verify",
|
|
64
|
+
"load_skill",
|
|
65
|
+
]);
|
|
66
|
+
/** Consecutive read-only tool calls that constitute a loop. */
|
|
67
|
+
const READ_ONLY_LOOP_THRESHOLD = 10;
|
|
38
68
|
export class StuckDetector {
|
|
39
69
|
threshold;
|
|
40
70
|
errorThreshold;
|
|
@@ -52,6 +82,10 @@ export class StuckDetector {
|
|
|
52
82
|
escalationThreshold = 3;
|
|
53
83
|
fileRewriteCount = new Map();
|
|
54
84
|
fileRewriteThreshold = 3;
|
|
85
|
+
lastBashCommand = "";
|
|
86
|
+
lastBashOutput = "";
|
|
87
|
+
emptyBashRunCount = 0;
|
|
88
|
+
readOnlyStreak = 0;
|
|
55
89
|
constructor(threshold = 6, errorThreshold = 3) {
|
|
56
90
|
this.threshold = threshold;
|
|
57
91
|
this.errorThreshold = errorThreshold;
|
|
@@ -70,6 +104,22 @@ export class StuckDetector {
|
|
|
70
104
|
if (this.recentToolCalls.length > this.maxRecentCalls) {
|
|
71
105
|
this.recentToolCalls.shift();
|
|
72
106
|
}
|
|
107
|
+
// Track the consecutive read-only streak. A write/exec tool resets it;
|
|
108
|
+
// plan/todo bookkeeping counts as read-only (it is not real progress).
|
|
109
|
+
if (READ_ONLY_TOOLS.has(name)) {
|
|
110
|
+
this.readOnlyStreak++;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
this.readOnlyStreak = 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* True after a long run of read-only tool calls with no write in between.
|
|
118
|
+
* The model is exploring/looping without producing anything — inject a
|
|
119
|
+
* recovery hint so it stops re-reading and actually edits files.
|
|
120
|
+
*/
|
|
121
|
+
hasReadOnlyLoop() {
|
|
122
|
+
return this.readOnlyStreak >= READ_ONLY_LOOP_THRESHOLD;
|
|
73
123
|
}
|
|
74
124
|
recordToolError(toolName, output) {
|
|
75
125
|
this.toolErrors.set(toolName, (this.toolErrors.get(toolName) || 0) + 1);
|
|
@@ -81,6 +131,28 @@ export class StuckDetector {
|
|
|
81
131
|
getLastErrorOutput() {
|
|
82
132
|
return this.lastErrorOutput;
|
|
83
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Record every bash invocation (successful or not) so stuck detection can
|
|
136
|
+
* reason about repeated runs with empty output, entry-point issues, and
|
|
137
|
+
* platform mistakes (PowerShell cmdlets in cmd.exe, bash heredocs, ...).
|
|
138
|
+
*/
|
|
139
|
+
recordBashOutput(command, output) {
|
|
140
|
+
const trimmed = output.trim();
|
|
141
|
+
if (command === this.lastBashCommand && !trimmed) {
|
|
142
|
+
this.emptyBashRunCount++;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
this.emptyBashRunCount = 0;
|
|
146
|
+
}
|
|
147
|
+
this.lastBashCommand = command;
|
|
148
|
+
this.lastBashOutput = output;
|
|
149
|
+
}
|
|
150
|
+
getLastBashCommand() {
|
|
151
|
+
return this.lastBashCommand;
|
|
152
|
+
}
|
|
153
|
+
getLastBashOutput() {
|
|
154
|
+
return this.lastBashOutput;
|
|
155
|
+
}
|
|
84
156
|
getLastFailedTool() {
|
|
85
157
|
return this.lastFailedTool;
|
|
86
158
|
}
|
|
@@ -158,9 +230,7 @@ export class StuckDetector {
|
|
|
158
230
|
getHints() {
|
|
159
231
|
const hints = [];
|
|
160
232
|
const desc = this.currentStepDescription.toLowerCase();
|
|
161
|
-
if (desc.includes("install") ||
|
|
162
|
-
desc.includes("npm") ||
|
|
163
|
-
desc.includes("pip")) {
|
|
233
|
+
if (desc.includes("install") || desc.includes("npm") || desc.includes("pip")) {
|
|
164
234
|
hints.push("Check if a lock file exists (package-lock.json, poetry.lock). If missing, run the install command first.");
|
|
165
235
|
}
|
|
166
236
|
if (desc.includes("test") || desc.includes("spec")) {
|
|
@@ -172,17 +242,32 @@ export class StuckDetector {
|
|
|
172
242
|
if (desc.includes("deploy") || desc.includes("publish")) {
|
|
173
243
|
hints.push("Verify credentials and network access before deploying.");
|
|
174
244
|
}
|
|
175
|
-
|
|
245
|
+
// State-based bullets. The recovery message (getRecoveryMessage) already
|
|
246
|
+
// reports the PRIMARY reason (priority: stuck > tool-errors > consecutive
|
|
247
|
+
// > repetitive > read-only). Repeating that same state here as a hint
|
|
248
|
+
// would duplicate the guidance in the same cooldown block. Only add the
|
|
249
|
+
// bullet when the recovery message is NOT already covering that state.
|
|
250
|
+
const primary = this.getPrimaryReason();
|
|
251
|
+
if (this.hasRepetitiveToolCalls() && primary !== "repetitive") {
|
|
176
252
|
hints.push("You are calling the same tool repeatedly with the same arguments. Try a different approach.");
|
|
177
253
|
}
|
|
178
|
-
if (this.hasConsecutiveFailures()) {
|
|
254
|
+
if (this.hasConsecutiveFailures() && primary !== "consecutive") {
|
|
179
255
|
hints.push("Multiple different tools are failing. Check if the environment is set up correctly.");
|
|
180
256
|
}
|
|
257
|
+
if (this.hasReadOnlyLoop() && primary !== "read-only") {
|
|
258
|
+
hints.push("You have made many read-only tool calls (read_file/glob/grep/browser) without writing anything. Stop exploring — make the edit/write the task needs, or use the plan tool to decide next steps.");
|
|
259
|
+
}
|
|
181
260
|
return hints;
|
|
182
261
|
}
|
|
183
262
|
getActionableHints() {
|
|
184
263
|
const hints = [];
|
|
185
264
|
const error = this.lastErrorOutput;
|
|
265
|
+
// Bash-specific hints do not depend on a tool failure — they fire even
|
|
266
|
+
// when the command "succeeded" with empty output (missing entry point).
|
|
267
|
+
const isScriptRun = /(^|[\s&])(bun|node|tsx|ts-node|deno|python|python3)\S*\s+[^|&]+\s+[^\s]+$/.test(this.lastBashCommand);
|
|
268
|
+
if (isScriptRun && !this.lastBashOutput.trim() && this.emptyBashRunCount >= 2) {
|
|
269
|
+
hints.push(`The command "${this.lastBashCommand}" ran ${this.emptyBashRunCount} times with EMPTY output. The program likely has no entry point — read the file with read_file and check that it actually calls its main function with process.argv / CLI arguments and prints results (console.log). Then run it again.`);
|
|
270
|
+
}
|
|
186
271
|
if (!error)
|
|
187
272
|
return hints;
|
|
188
273
|
// Runtime incompatibility — tool/module crashes on this Node version
|
|
@@ -220,6 +305,25 @@ export class StuckDetector {
|
|
|
220
305
|
const file = this.getExcessiveRewriteFile();
|
|
221
306
|
hints.push(`File ${file} has been rewritten ${this.getFileRewriteCount(file)} times without success. Stop rewriting and try a fundamentally different approach.`);
|
|
222
307
|
}
|
|
308
|
+
// Repeated identical bash runs with empty output — likely a missing
|
|
309
|
+
// entry point (the program never calls its main function).
|
|
310
|
+
// PowerShell cmdlets used inside cmd.exe (Windows shell mismatch).
|
|
311
|
+
if (/Write-Host|Get-Content|Select-String|Out-File|Set-Content/i.test(error)) {
|
|
312
|
+
hints.push("That looks like a PowerShell cmdlet — the shell here is cmd.exe. Use echo/type for output or the read_file/write_file tools instead.");
|
|
313
|
+
}
|
|
314
|
+
// Model invented a Bun API that does not exist.
|
|
315
|
+
if (/Bun\.\w+ is not a function|Bun\.\w+ is not defined|Bun\.\w+ is not a constructor/i.test(error)) {
|
|
316
|
+
hints.push("That Bun API does not exist. Verify the API name in the Bun docs — common ones are Bun.file, Bun.write, Bun.spawn, Bun.serve. For file checks use fs.existsSync from node:fs.");
|
|
317
|
+
}
|
|
318
|
+
// The model keeps writing a file whose output shows a type/syntax
|
|
319
|
+
// error — read the actual error and fix it, don't rewrite blindly.
|
|
320
|
+
if (/error TS\d+|typecheck failed|syntax check failed/i.test(error)) {
|
|
321
|
+
hints.push("The file still has a type/syntax error (see the error line in the output). Read the file with read_file around the reported line, fix the actual error, then re-run — rewriting the whole file blindly usually makes it worse.");
|
|
322
|
+
}
|
|
323
|
+
// Bash heredoc in cmd.exe.
|
|
324
|
+
if (/unexpected.*<<|Непредвиденное появление/i.test(error)) {
|
|
325
|
+
hints.push("Heredoc (<< EOF) is a bash feature — this shell is cmd.exe and does not support it. Write the file with write_file instead.");
|
|
326
|
+
}
|
|
223
327
|
return hints;
|
|
224
328
|
}
|
|
225
329
|
getToolAlternative() {
|
|
@@ -287,10 +391,16 @@ export class StuckDetector {
|
|
|
287
391
|
if (errorTool) {
|
|
288
392
|
return t("exec.tool_errors", { tool: errorTool[0], count: errorTool[1] });
|
|
289
393
|
}
|
|
394
|
+
if (this.hasReadOnlyLoop()) {
|
|
395
|
+
return t("exec.read_only_loop", {
|
|
396
|
+
count: String(this.readOnlyStreak),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
290
399
|
return "";
|
|
291
400
|
}
|
|
292
401
|
getRecoveryMessage() {
|
|
293
|
-
|
|
402
|
+
const reason = this.getPrimaryReason();
|
|
403
|
+
if (reason === "stuck") {
|
|
294
404
|
return t("exec.stuck_recovery", {
|
|
295
405
|
iterations: this.iterationsOnCurrentStep,
|
|
296
406
|
stepId: String(this.currentStepId ?? "?"),
|
|
@@ -298,24 +408,50 @@ export class StuckDetector {
|
|
|
298
408
|
});
|
|
299
409
|
}
|
|
300
410
|
// Per-tool error recovery (more specific — e.g., "bash failed 3 times")
|
|
301
|
-
|
|
302
|
-
|
|
411
|
+
if (reason === "tool-errors") {
|
|
412
|
+
const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
|
|
303
413
|
return t("exec.tool_errors_recovery", {
|
|
304
414
|
tool: errorTool[0],
|
|
305
415
|
count: errorTool[1],
|
|
306
416
|
});
|
|
307
417
|
}
|
|
308
418
|
// Consecutive failures from different tools (generic — e.g., "5 consecutive failures")
|
|
309
|
-
if (
|
|
419
|
+
if (reason === "consecutive") {
|
|
310
420
|
return t("exec.consecutive_failures_recovery", {
|
|
311
421
|
count: this.consecutiveFailures,
|
|
312
422
|
});
|
|
313
423
|
}
|
|
314
|
-
if (
|
|
424
|
+
if (reason === "repetitive") {
|
|
315
425
|
return this.getRepetitiveToolMessage();
|
|
316
426
|
}
|
|
427
|
+
if (reason === "read-only") {
|
|
428
|
+
return t("exec.read_only_loop_recovery", {
|
|
429
|
+
count: String(this.readOnlyStreak),
|
|
430
|
+
});
|
|
431
|
+
}
|
|
317
432
|
return "";
|
|
318
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* Single source of truth for the recovery reason priority. Used by BOTH
|
|
436
|
+
* getRecoveryMessage() and getHints() so the two cannot drift apart: the
|
|
437
|
+
* state bullet that getRecoveryMessage already reports is NOT repeated as a
|
|
438
|
+
* hint (priority: stuck > tool-errors > consecutive > repetitive >
|
|
439
|
+
* read-only).
|
|
440
|
+
*/
|
|
441
|
+
getPrimaryReason() {
|
|
442
|
+
if (this.isStuck())
|
|
443
|
+
return "stuck";
|
|
444
|
+
const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
|
|
445
|
+
if (errorTool)
|
|
446
|
+
return "tool-errors";
|
|
447
|
+
if (this.hasConsecutiveFailures())
|
|
448
|
+
return "consecutive";
|
|
449
|
+
if (this.hasRepetitiveToolCalls())
|
|
450
|
+
return "repetitive";
|
|
451
|
+
if (this.hasReadOnlyLoop())
|
|
452
|
+
return "read-only";
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
319
455
|
reset() {
|
|
320
456
|
this.currentStepId = null;
|
|
321
457
|
this.iterationsOnCurrentStep = 0;
|
|
@@ -326,6 +462,29 @@ export class StuckDetector {
|
|
|
326
462
|
this.recentToolCalls = [];
|
|
327
463
|
this.fileRewriteCount.clear();
|
|
328
464
|
this.escalationCount = 0;
|
|
465
|
+
this.lastBashCommand = "";
|
|
466
|
+
this.lastBashOutput = "";
|
|
467
|
+
this.emptyBashRunCount = 0;
|
|
468
|
+
this.readOnlyStreak = 0;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Reset only step-progress state, keeping the tool-call history and the
|
|
472
|
+
* read-only streak. Used when the plan is complete but the loop continues
|
|
473
|
+
* (e.g. the audit gate rejected a finished task): step-stuck warnings are
|
|
474
|
+
* silenced, but a read-without-write loop is still detected.
|
|
475
|
+
*/
|
|
476
|
+
resetStepProgress() {
|
|
477
|
+
this.currentStepId = null;
|
|
478
|
+
this.iterationsOnCurrentStep = 0;
|
|
479
|
+
this.toolErrors.clear();
|
|
480
|
+
this.consecutiveFailures = 0;
|
|
481
|
+
this.lastFailedTool = "";
|
|
482
|
+
this.lastErrorOutput = "";
|
|
483
|
+
this.fileRewriteCount.clear();
|
|
484
|
+
this.escalationCount = 0;
|
|
485
|
+
this.lastBashCommand = "";
|
|
486
|
+
this.lastBashOutput = "";
|
|
487
|
+
this.emptyBashRunCount = 0;
|
|
329
488
|
}
|
|
330
489
|
/**
|
|
331
490
|
* Reset all per-step state when moving to a new step. Prevents errors and
|
|
@@ -341,6 +500,10 @@ export class StuckDetector {
|
|
|
341
500
|
this.lastErrorOutput = "";
|
|
342
501
|
this.recentToolCalls = [];
|
|
343
502
|
this.fileRewriteCount.clear();
|
|
503
|
+
this.lastBashCommand = "";
|
|
504
|
+
this.lastBashOutput = "";
|
|
505
|
+
this.emptyBashRunCount = 0;
|
|
506
|
+
this.readOnlyStreak = 0;
|
|
344
507
|
// Preserve escalationCount — prevents agent from "gaming" stuck detection
|
|
345
508
|
// by marking a step done and moving to the next one
|
|
346
509
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { existsSync } from
|
|
2
|
-
import { resolve, extname } from
|
|
3
|
-
import { spawn } from
|
|
4
|
-
import { t } from
|
|
5
|
-
import { validateExpertConfig } from
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { resolve, extname, join } from "path";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import { t } from "../../i18n/index";
|
|
5
|
+
import { validateExpertConfig } from "../../config/experts";
|
|
6
|
+
import { findProjectRoot } from "../lsp/project-root";
|
|
7
|
+
import { extractFileLikeTokens, stripUrls } from "../hallucination/js-identifiers";
|
|
8
|
+
import { findExistingFile } from "./auditor";
|
|
6
9
|
export class StepVerifier {
|
|
7
10
|
baseDir;
|
|
8
11
|
constructor(baseDir) {
|
|
@@ -13,26 +16,51 @@ export class StepVerifier {
|
|
|
13
16
|
const exists = existsSync(resolved);
|
|
14
17
|
return {
|
|
15
18
|
passed: exists,
|
|
16
|
-
message: exists ? t(
|
|
19
|
+
message: exists ? t("verify.file_exists", { path }) : t("verify.file_not_found", { path }),
|
|
17
20
|
};
|
|
18
21
|
}
|
|
19
22
|
async runScript(scriptName) {
|
|
20
23
|
try {
|
|
21
24
|
await this.runAsync(`bun run ${scriptName}`, this.baseDir, 60_000);
|
|
22
|
-
return { passed: true, message: t(
|
|
25
|
+
return { passed: true, message: t("verify.script_passed", { script: scriptName }) };
|
|
23
26
|
}
|
|
24
27
|
catch (e) {
|
|
25
|
-
return {
|
|
28
|
+
return {
|
|
29
|
+
passed: false,
|
|
30
|
+
message: t("verify.script_failed", { script: scriptName, message: e.message }),
|
|
31
|
+
};
|
|
26
32
|
}
|
|
27
33
|
}
|
|
28
34
|
async runTypeCheck() {
|
|
29
|
-
const tsconfigPath = resolve(this.baseDir,
|
|
35
|
+
const tsconfigPath = resolve(this.baseDir, "tsconfig.json");
|
|
30
36
|
if (!existsSync(tsconfigPath)) {
|
|
31
|
-
return { passed: true, message:
|
|
37
|
+
return { passed: true, message: "No tsconfig.json found — skipping type check" };
|
|
32
38
|
}
|
|
33
39
|
try {
|
|
34
|
-
await this.runAsync(
|
|
35
|
-
return { passed: true, message:
|
|
40
|
+
await this.runAsync("npx tsc --noEmit", this.baseDir, 60_000);
|
|
41
|
+
return { passed: true, message: "TypeScript type check passed" };
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
const stderr = e.stderr?.toString() || e.stdout?.toString() || e.message;
|
|
45
|
+
return { passed: false, message: `TypeScript type check failed: ${stderr.slice(0, 500)}` };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Type-check a specific file, resolving the project root from the FILE's
|
|
50
|
+
* location (not the agent baseDir). For nested layouts (`baseDir/proj/` with
|
|
51
|
+
* its own tsconfig.json) the old baseDir-based check silently skipped every
|
|
52
|
+
* type error (observed in ses_mst2r0r5: zero type feedback for the whole
|
|
53
|
+
* session). Markers mirror the TypeScript LSP server config.
|
|
54
|
+
*/
|
|
55
|
+
async runTypeCheckForFile(filePath) {
|
|
56
|
+
const projectRoot = findProjectRoot(filePath, this.baseDir, ["tsconfig.json", "package.json"]);
|
|
57
|
+
const tsconfigPath = join(projectRoot, "tsconfig.json");
|
|
58
|
+
if (!existsSync(tsconfigPath)) {
|
|
59
|
+
return { passed: true, message: "No tsconfig.json found — skipping type check" };
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
await this.runAsync("npx tsc --noEmit --skipLibCheck", projectRoot, 60_000);
|
|
63
|
+
return { passed: true, message: "TypeScript type check passed" };
|
|
36
64
|
}
|
|
37
65
|
catch (e) {
|
|
38
66
|
const stderr = e.stderr?.toString() || e.stdout?.toString() || e.message;
|
|
@@ -40,21 +68,23 @@ export class StepVerifier {
|
|
|
40
68
|
}
|
|
41
69
|
}
|
|
42
70
|
async runTests() {
|
|
43
|
-
const pkgPath = resolve(this.baseDir,
|
|
71
|
+
const pkgPath = resolve(this.baseDir, "package.json");
|
|
44
72
|
if (!existsSync(pkgPath)) {
|
|
45
|
-
return { passed: true, message:
|
|
73
|
+
return { passed: true, message: "No package.json found — skipping tests" };
|
|
46
74
|
}
|
|
47
75
|
try {
|
|
48
|
-
const pkg = JSON.parse(require(
|
|
76
|
+
const pkg = JSON.parse(require("fs").readFileSync(pkgPath, "utf-8"));
|
|
49
77
|
if (!pkg.scripts?.test) {
|
|
50
|
-
return { passed: true, message:
|
|
78
|
+
return { passed: true, message: "No test script defined — skipping tests" };
|
|
51
79
|
}
|
|
52
80
|
}
|
|
53
|
-
catch {
|
|
54
|
-
|
|
81
|
+
catch {
|
|
82
|
+
/* fall through — attempt to run */
|
|
83
|
+
}
|
|
84
|
+
return this.runScript("test");
|
|
55
85
|
}
|
|
56
86
|
async verifyArtifactFiles(files) {
|
|
57
|
-
return Promise.all(files.map(f => this.checkFileExists(f)));
|
|
87
|
+
return Promise.all(files.map((f) => this.checkFileExists(f)));
|
|
58
88
|
}
|
|
59
89
|
async verifyMoEManifest(plan, config, allToolTags) {
|
|
60
90
|
const errors = [];
|
|
@@ -87,45 +117,55 @@ export class StepVerifier {
|
|
|
87
117
|
};
|
|
88
118
|
}
|
|
89
119
|
async verifyStep(stepDescription) {
|
|
90
|
-
const fileMatches = stepDescription
|
|
120
|
+
const fileMatches = extractFileLikeTokens(stripUrls(stepDescription));
|
|
121
|
+
// No named files — this step cannot be verified by file existence. Do NOT
|
|
122
|
+
// trivially pass (empty every() = true); signal noFiles so the tool guides
|
|
123
|
+
// the model to run a real check instead.
|
|
124
|
+
if (fileMatches.length === 0) {
|
|
125
|
+
return { passed: false, syntaxValid: true, failed: [], noFiles: true };
|
|
126
|
+
}
|
|
91
127
|
const results = [];
|
|
92
128
|
let syntaxValid = true;
|
|
93
129
|
for (const filePath of fileMatches) {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
130
|
+
const resolved = findExistingFile(this.baseDir, filePath);
|
|
131
|
+
if (!resolved) {
|
|
132
|
+
results.push({ passed: false, message: t("verify.file_not_found", { path: filePath }) });
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
results.push({ passed: true, message: t("verify.file_exists", { path: filePath }) });
|
|
136
|
+
if (!(await this.validateSyntax(resolved))) {
|
|
137
|
+
syntaxValid = false;
|
|
138
|
+
results.push({ passed: false, message: t("verify.syntax_error", { path: filePath }) });
|
|
102
139
|
}
|
|
103
140
|
}
|
|
104
141
|
return {
|
|
105
|
-
passed: results.every(r => r.passed),
|
|
142
|
+
passed: results.every((r) => r.passed),
|
|
106
143
|
syntaxValid,
|
|
107
|
-
failed: results.filter(r => !r.passed),
|
|
144
|
+
failed: results.filter((r) => !r.passed),
|
|
145
|
+
noFiles: false,
|
|
108
146
|
};
|
|
109
147
|
}
|
|
110
148
|
async validateSyntax(filePath) {
|
|
111
149
|
const ext = extname(filePath);
|
|
112
|
-
if (ext ===
|
|
150
|
+
if (ext === ".ts" || ext === ".tsx" || ext === ".cts" || ext === ".mts") {
|
|
113
151
|
try {
|
|
114
152
|
await this.runAsync(`npx tsc --noEmit --skipLibCheck ${filePath}`, this.baseDir, 10000);
|
|
115
153
|
return true;
|
|
116
154
|
}
|
|
117
155
|
catch (err) {
|
|
118
|
-
if (err.status === 127 ||
|
|
156
|
+
if (err.status === 127 ||
|
|
157
|
+
err.message.includes("not found") ||
|
|
158
|
+
err.message.includes("ENOENT")) {
|
|
119
159
|
return true;
|
|
120
160
|
}
|
|
121
|
-
const stderr = err.stderr?.toString() ||
|
|
122
|
-
if (stderr.includes(
|
|
161
|
+
const stderr = err.stderr?.toString() || "";
|
|
162
|
+
if (stderr.includes("error TS") && !stderr.includes("Cannot find module")) {
|
|
123
163
|
return false;
|
|
124
164
|
}
|
|
125
165
|
return true;
|
|
126
166
|
}
|
|
127
167
|
}
|
|
128
|
-
if (ext ===
|
|
168
|
+
if (ext === ".js" || ext === ".jsx" || ext === ".cjs" || ext === ".mjs") {
|
|
129
169
|
try {
|
|
130
170
|
await this.runAsync(`node --check ${filePath}`, this.baseDir, 5000);
|
|
131
171
|
return true;
|
|
@@ -146,21 +186,25 @@ export class StepVerifier {
|
|
|
146
186
|
cwd,
|
|
147
187
|
shell: true,
|
|
148
188
|
windowsHide: true,
|
|
149
|
-
stdio: [
|
|
189
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
190
|
+
});
|
|
191
|
+
let stdout = "";
|
|
192
|
+
let stderr = "";
|
|
193
|
+
child.stdout?.on("data", (d) => {
|
|
194
|
+
stdout += d.toString();
|
|
195
|
+
});
|
|
196
|
+
child.stderr?.on("data", (d) => {
|
|
197
|
+
stderr += d.toString();
|
|
150
198
|
});
|
|
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
199
|
const timer = setTimeout(() => {
|
|
156
200
|
child.kill();
|
|
157
201
|
reject(new Error(`Command timed out after ${timeoutMs}ms`));
|
|
158
202
|
}, timeoutMs);
|
|
159
|
-
child.on(
|
|
203
|
+
child.on("error", (err) => {
|
|
160
204
|
clearTimeout(timer);
|
|
161
205
|
reject(err);
|
|
162
206
|
});
|
|
163
|
-
child.on(
|
|
207
|
+
child.on("close", (code) => {
|
|
164
208
|
clearTimeout(timer);
|
|
165
209
|
if (code === 0) {
|
|
166
210
|
resolve({ stdout, stderr });
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unix commands that do not exist (or have incompatible semantics) in Windows
|
|
3
|
+
* cmd.exe. The 9B model keeps reaching for grep/sed/ls/find via the bash tool;
|
|
4
|
+
* each failure returns a hint, but the model retries.
|
|
5
|
+
*
|
|
6
|
+
* This is a session-level, module-side companion to the bash tool's own
|
|
7
|
+
* per-command hard-stop (`UNIX_TO_WIN_HINTS` / `FAILING_FIRST_WORDS` /
|
|
8
|
+
* `HARD_BLOCK_THRESHOLD` in src/tools/bash.ts). That mechanism injects
|
|
9
|
+
* "STOP using X" INTO the tool output, which a 9B model tends to ignore; this
|
|
10
|
+
* one, gated to win32 like bash.ts, queues a <system-summary> that lands
|
|
11
|
+
* before the next model call. Keep the two command lists consistent.
|
|
12
|
+
*
|
|
13
|
+
* This is a command allow/deny list (like the security module's command
|
|
14
|
+
* blacklist), NOT task classification (rule #10).
|
|
15
|
+
*/
|
|
16
|
+
const FORBIDDEN_COMMANDS = new Set([
|
|
17
|
+
"grep",
|
|
18
|
+
"sed",
|
|
19
|
+
"ls",
|
|
20
|
+
"find",
|
|
21
|
+
"rm",
|
|
22
|
+
"touch",
|
|
23
|
+
"which",
|
|
24
|
+
"diff",
|
|
25
|
+
"cp",
|
|
26
|
+
"mv",
|
|
27
|
+
]);
|
|
28
|
+
function firstWord(command) {
|
|
29
|
+
const segs = command.trim().split(/[\s;|&]+/);
|
|
30
|
+
return (segs[0] || "").replace(/[^\w-]/g, "").toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
/** The forbidden command name if `command` starts with one (handles `cd x && …`). */
|
|
33
|
+
export function forbiddenWindowsCommand(command) {
|
|
34
|
+
const m = command.match(/^\s*cd\s+\S+\s*&&\s*(\S+)/);
|
|
35
|
+
if (m)
|
|
36
|
+
return forbiddenWindowsCommand(m[1]);
|
|
37
|
+
const word = firstWord(command);
|
|
38
|
+
if (word && FORBIDDEN_COMMANDS.has(word))
|
|
39
|
+
return word;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
@@ -8,7 +8,11 @@ export class ConfidenceCheck {
|
|
|
8
8
|
}
|
|
9
9
|
validate(response) {
|
|
10
10
|
if (!response || response.length < MIN_CHARS) {
|
|
11
|
-
return {
|
|
11
|
+
return {
|
|
12
|
+
status: "retry",
|
|
13
|
+
kind: "short",
|
|
14
|
+
reason: t("hall.short_response"),
|
|
15
|
+
};
|
|
12
16
|
}
|
|
13
17
|
// Language-agnostic: very short response with no structured content
|
|
14
18
|
const wordCount = response.split(/\s+/).filter(Boolean).length;
|
|
@@ -16,6 +20,7 @@ export class ConfidenceCheck {
|
|
|
16
20
|
if (wordCount < MIN_WORDS && !hasStructure) {
|
|
17
21
|
return {
|
|
18
22
|
status: "warn",
|
|
23
|
+
kind: "short",
|
|
19
24
|
reason: t("hall.short_response"),
|
|
20
25
|
};
|
|
21
26
|
}
|
|
@@ -25,6 +30,7 @@ export class ConfidenceCheck {
|
|
|
25
30
|
if (overlap > 0.5) {
|
|
26
31
|
return {
|
|
27
32
|
status: "retry",
|
|
33
|
+
kind: "repetition",
|
|
28
34
|
reason: t("hall.repetitive", { pct: Math.round(overlap * 100) }),
|
|
29
35
|
};
|
|
30
36
|
}
|
|
@@ -40,6 +46,7 @@ export class ConfidenceCheck {
|
|
|
40
46
|
if (diversity < 0.25) {
|
|
41
47
|
return {
|
|
42
48
|
status: "warn",
|
|
49
|
+
kind: "diversity",
|
|
43
50
|
reason: t("hall.repetitive", {
|
|
44
51
|
pct: Math.round((1 - diversity) * 100),
|
|
45
52
|
}),
|
|
@@ -21,8 +21,7 @@ export class HallucinationDetector {
|
|
|
21
21
|
}
|
|
22
22
|
async validate(response) {
|
|
23
23
|
const confidenceResult = this.confidence.validate(response);
|
|
24
|
-
if (confidenceResult.status === "retry" ||
|
|
25
|
-
confidenceResult.status === "block") {
|
|
24
|
+
if (confidenceResult.status === "retry" || confidenceResult.status === "block") {
|
|
26
25
|
return confidenceResult;
|
|
27
26
|
}
|
|
28
27
|
const factualResult = this.factual?.validate(response);
|
|
@@ -30,9 +29,7 @@ export class HallucinationDetector {
|
|
|
30
29
|
return factualResult;
|
|
31
30
|
}
|
|
32
31
|
// LLM-as-judge consistency: fast path (no decisions) skips any LLM call.
|
|
33
|
-
const judgeResult = this.judge
|
|
34
|
-
? await this.judge.validate(response, this.consistency)
|
|
35
|
-
: null;
|
|
32
|
+
const judgeResult = this.judge ? await this.judge.validate(response, this.consistency) : null;
|
|
36
33
|
const warnings = [];
|
|
37
34
|
if (judgeResult && judgeResult.status === "warn")
|
|
38
35
|
warnings.push(judgeResult.reason || "");
|