micro-models-agent 0.63.0 → 1.1.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/CHANGELOG.md +174 -0
- package/dist/cli/cache-line.js +30 -0
- package/dist/cli/command-suggest.js +38 -0
- package/dist/cli/commands.js +285 -60
- package/dist/cli/completer.js +16 -16
- package/dist/cli/json-payload.js +32 -0
- package/dist/cli/main.js +165 -77
- package/dist/cli/plugin-commands.js +5 -4
- package/dist/cli/relaunch.js +37 -0
- package/dist/cli/repl-commands.js +441 -307
- package/dist/cli/repl.js +360 -83
- package/dist/cli/run-result.js +12 -6
- package/dist/cli/security-commands.js +64 -60
- package/dist/cli/setup-order.js +57 -0
- package/dist/cli/setup-prompt.js +49 -0
- package/dist/cli/setup.js +52 -48
- package/dist/config/budget.js +48 -0
- package/dist/config/config.js +132 -70
- package/dist/config/defaults.js +37 -11
- package/dist/config/domains.js +9 -50
- package/dist/config/utils.js +56 -0
- package/dist/core/agent/audit-gate.js +49 -0
- package/dist/core/agent/compaction.js +89 -0
- package/dist/core/agent/constants.js +61 -0
- package/dist/core/agent/context-renderer.js +40 -0
- package/dist/core/agent/hallucination-gate.js +87 -0
- package/dist/core/agent/loop-state.js +53 -0
- package/dist/core/agent/prefix-monitor.js +101 -0
- package/dist/core/agent/reasoning-resolver.js +56 -0
- package/dist/core/agent/token-tracker.js +96 -0
- package/dist/core/agent/tool-batch.js +237 -0
- package/dist/core/agent/tool-output.js +62 -0
- package/dist/core/agent-moe.js +214 -69
- package/dist/core/agent.js +506 -546
- package/dist/core/bootstrap.js +297 -98
- package/dist/core/crash-handler.js +2 -1
- package/dist/core/prompt-builder.js +3 -0
- package/dist/core/prompt-overflow.js +307 -0
- package/dist/core/session-logger.js +34 -2
- package/dist/i18n/en.json +8 -4
- package/dist/i18n/ru.json +8 -4
- package/dist/index.js +5 -1
- package/dist/llm/cache-usage.js +76 -0
- package/dist/llm/image-utils.js +20 -16
- package/dist/llm/llm-errors.js +41 -0
- package/dist/llm/model-loader.js +30 -0
- package/dist/llm/openai-compat.js +287 -101
- package/dist/llm/orchestrator.js +140 -68
- package/dist/llm/provider-budget.js +68 -0
- package/dist/llm/provider.js +0 -1
- package/dist/llm/stream-state.js +26 -0
- package/dist/llm/token-counter.js +28 -0
- package/dist/logger/app-logger.js +12 -15
- package/dist/main.js +1755 -841
- package/dist/migration/detect.js +3 -1
- package/dist/modules/browser/actions.js +0 -3
- package/dist/modules/browser/bridge-client.js +2 -0
- package/dist/modules/browser/bridge-server.mjs +37 -4
- package/dist/modules/browser/driver.js +46 -4
- package/dist/modules/certification/cli.js +85 -42
- package/dist/modules/certification/loader.js +15 -1
- package/dist/modules/certification/manifest.js +126 -15
- package/dist/modules/certification/runner.js +4 -26
- package/dist/modules/certification/scenarios.js +184 -5
- package/dist/modules/certification/syntax-scenarios.js +51 -0
- package/dist/modules/context/chunk-query.js +25 -5
- package/dist/modules/context/fact-extractor.js +6 -2
- package/dist/modules/context/manager.js +23 -7
- package/dist/modules/execution/audit-runners.js +7 -1
- package/dist/modules/execution/auditor.js +3 -3
- package/dist/modules/execution/execution-plugin.js +22 -15
- package/dist/modules/execution/input-from.js +46 -0
- package/dist/modules/execution/module.js +107 -18
- package/dist/modules/execution/moe-executor.js +166 -54
- package/dist/modules/execution/plan-actions.js +524 -0
- package/dist/modules/execution/plan-steps.js +23 -0
- package/dist/modules/execution/plan-store.js +15 -3
- package/dist/modules/execution/plan-tool.js +6 -488
- package/dist/modules/execution/plan-validator.js +24 -0
- package/dist/modules/execution/stuck-detector.js +3 -18
- package/dist/modules/execution/tracker.js +14 -5
- package/dist/modules/execution/transient-error.js +30 -0
- package/dist/modules/execution/verifier.js +94 -7
- package/dist/modules/execution/windows-commands.js +11 -0
- package/dist/modules/hallucination/confidence.js +36 -23
- package/dist/modules/hallucination/consistency.js +3 -0
- package/dist/modules/hallucination/detector.js +8 -3
- package/dist/modules/hallucination/factual.js +26 -7
- package/dist/modules/hallucination/llm-judge.js +12 -2
- package/dist/modules/indexer/map-command.js +35 -0
- package/dist/modules/indexer/map-select.js +87 -0
- package/dist/modules/indexer/module.js +34 -22
- package/dist/modules/indexer/symbols.js +189 -0
- package/dist/modules/indexer/walker.js +96 -42
- package/dist/modules/lsp/check-tool.js +2 -1
- package/dist/modules/lsp/client.js +49 -32
- package/dist/modules/lsp/config.js +55 -2
- package/dist/modules/lsp/module.js +38 -5
- package/dist/modules/lsp/probe.js +4 -3
- package/dist/modules/lsp/project-root.js +41 -1
- package/dist/modules/lsp/startup-check.js +12 -4
- package/dist/modules/mcp/client.js +153 -104
- package/dist/modules/mcp/module.js +165 -41
- package/dist/modules/memory/module.js +4 -3
- package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
- package/dist/modules/plugins/manager.js +47 -84
- package/dist/modules/pricing/index.js +17 -7
- package/dist/modules/pricing/prices.js +30 -12
- package/dist/modules/processes/index.js +1 -0
- package/dist/modules/processes/kill-tree.js +56 -0
- package/dist/modules/processes/registry.js +2 -54
- package/dist/modules/providers/cache.js +23 -0
- package/dist/modules/providers/factory.js +28 -0
- package/dist/modules/providers/fallback.js +7 -5
- package/dist/modules/providers/health.js +2 -1
- package/dist/modules/providers/index.js +1 -0
- package/dist/modules/providers/manager.js +17 -2
- package/dist/modules/providers/presets.js +79 -6
- package/dist/modules/reasoning/policy.js +40 -0
- package/dist/modules/reasoning/probe.js +111 -0
- package/dist/modules/security/audit-notifier.js +42 -27
- package/dist/modules/security/command-validator.js +25 -20
- package/dist/modules/security/encryption.js +6 -12
- package/dist/modules/security/network-validator.js +76 -5
- package/dist/modules/security/path-validator.js +77 -34
- package/dist/modules/security/rate-limiter.js +11 -0
- package/dist/modules/security/security-policies.js +1 -1
- package/dist/modules/security/session-encryption.js +13 -2
- package/dist/modules/security/session-isolation.js +2 -9
- package/dist/modules/session/manager.js +11 -0
- package/dist/modules/session/module.js +11 -3
- package/dist/modules/session/store.js +41 -5
- package/dist/modules/skills/loader.js +7 -1
- package/dist/modules/skills/module.js +2 -1
- package/dist/modules/updater/changelog-reader.js +94 -0
- package/dist/modules/updater/dev-detect.js +17 -0
- package/dist/modules/updater/index.js +1 -0
- package/dist/modules/updater/module.js +14 -3
- package/dist/output/bus.js +32 -0
- package/dist/output/channel.js +233 -0
- package/dist/output/format.js +14 -0
- package/dist/output/index.js +7 -0
- package/dist/output/json-sink.js +22 -0
- package/dist/output/machine.js +8 -0
- package/dist/output/session-sink.js +27 -0
- package/dist/output/types.js +1 -0
- package/dist/tools/approve.js +6 -2
- package/dist/tools/attach-image.js +11 -11
- package/dist/tools/auto-fixer.js +198 -0
- package/dist/tools/bash.js +142 -89
- package/dist/tools/chunk-query.js +10 -6
- package/dist/tools/download-file.js +1 -1
- package/dist/tools/edit-file.js +20 -2
- package/dist/tools/executor.js +54 -9
- package/dist/tools/glob-tool.js +7 -0
- package/dist/tools/grep-tool.js +15 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/list-dir.js +3 -1
- package/dist/tools/load-skill.js +2 -1
- package/dist/tools/mcp-call.js +1 -1
- package/dist/tools/move-file.js +5 -4
- package/dist/tools/path-utils.js +7 -0
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/prompt-io.js +28 -0
- package/dist/tools/question.js +12 -12
- package/dist/tools/scope-request.js +91 -0
- package/dist/tools/session-info.js +44 -0
- package/dist/tools/set-thinking.js +71 -0
- package/dist/tools/subagent.js +50 -9
- package/dist/tools/syntax-validator.js +177 -0
- package/dist/tools/user-input.js +16 -9
- package/dist/tools/write-file.js +17 -1
- package/dist/ui/diff.js +10 -0
- package/dist/ui/line-editor.js +179 -26
- package/dist/ui/line-math.js +20 -3
- package/dist/ui/md-formatter.js +100 -10
- package/dist/ui/output.js +5 -4
- package/dist/ui/plan-view.js +2 -7
- package/dist/ui/renderer.js +89 -85
- package/dist/ui/spinner.js +14 -4
- package/dist/utils/error.js +4 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/retry.js +17 -0
- package/dist/utils/sleep.js +23 -0
- package/dist/utils/truncate.js +9 -0
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { existsSync } from "fs";
|
|
2
|
-
import { resolve, extname, join } from "path";
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { resolve, extname, dirname, join } from "path";
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
4
|
import { t } from "../../i18n/index";
|
|
5
5
|
import { validateExpertConfig } from "../../config/experts";
|
|
6
|
-
import { findProjectRoot } from "../lsp/project-root";
|
|
6
|
+
import { findProjectRoot, resolveTscCommand } from "../lsp/project-root";
|
|
7
7
|
import { extractFileLikeTokens, stripUrls } from "../hallucination/js-identifiers";
|
|
8
8
|
import { findExistingFile } from "./auditor";
|
|
9
9
|
export class StepVerifier {
|
|
@@ -36,8 +36,11 @@ export class StepVerifier {
|
|
|
36
36
|
if (!existsSync(tsconfigPath)) {
|
|
37
37
|
return { passed: true, message: "No tsconfig.json found — skipping type check" };
|
|
38
38
|
}
|
|
39
|
+
const tsc = resolveTscCommand(this.baseDir);
|
|
40
|
+
if (!tsc)
|
|
41
|
+
return { passed: true, message: "tsc not installed — skipping type check" };
|
|
39
42
|
try {
|
|
40
|
-
await this.runAsync(
|
|
43
|
+
await this.runAsync(`${tsc} --noEmit`, this.baseDir, 60_000);
|
|
41
44
|
return { passed: true, message: "TypeScript type check passed" };
|
|
42
45
|
}
|
|
43
46
|
catch (e) {
|
|
@@ -58,8 +61,11 @@ export class StepVerifier {
|
|
|
58
61
|
if (!existsSync(tsconfigPath)) {
|
|
59
62
|
return { passed: true, message: "No tsconfig.json found — skipping type check" };
|
|
60
63
|
}
|
|
64
|
+
const tsc = resolveTscCommand(projectRoot);
|
|
65
|
+
if (!tsc)
|
|
66
|
+
return { passed: true, message: "tsc not installed — skipping type check" };
|
|
61
67
|
try {
|
|
62
|
-
await this.runAsync(
|
|
68
|
+
await this.runAsync(`${tsc} --noEmit --skipLibCheck`, projectRoot, 60_000);
|
|
63
69
|
return { passed: true, message: "TypeScript type check passed" };
|
|
64
70
|
}
|
|
65
71
|
catch (e) {
|
|
@@ -86,6 +92,75 @@ export class StepVerifier {
|
|
|
86
92
|
async verifyArtifactFiles(files) {
|
|
87
93
|
return Promise.all(files.map((f) => this.checkFileExists(f)));
|
|
88
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Check a subtask's machine-readable success_criteria BEFORE the merge
|
|
97
|
+
* decision (Plan 5.1). Supported forms:
|
|
98
|
+
* - `file-exists:<path>`
|
|
99
|
+
* - `substring-in-file:<path>:<text>`
|
|
100
|
+
* - `command-exit-0:<command>`
|
|
101
|
+
* Anything else is treated as an LLM-only criterion and reported as a
|
|
102
|
+
* warning (never an error) so the Router still sees it in context.
|
|
103
|
+
*/
|
|
104
|
+
async verifySubtaskCriteria(subtask) {
|
|
105
|
+
const errors = [];
|
|
106
|
+
const warnings = [];
|
|
107
|
+
const details = [];
|
|
108
|
+
for (const criterion of subtask.success_criteria || []) {
|
|
109
|
+
const sep = criterion.indexOf(":");
|
|
110
|
+
if (sep <= 0) {
|
|
111
|
+
warnings.push(`Subtask "${subtask.id}": non-machine criterion "${criterion}" (LLM-only)`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const kind = criterion.slice(0, sep);
|
|
115
|
+
const rest = criterion.slice(sep + 1);
|
|
116
|
+
if (kind === "file-exists") {
|
|
117
|
+
const check = await this.checkFileExists(rest);
|
|
118
|
+
details.push(check);
|
|
119
|
+
if (!check.passed)
|
|
120
|
+
errors.push(`[${subtask.id}] ${check.message}`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (kind === "substring-in-file") {
|
|
124
|
+
const colon = rest.indexOf(":");
|
|
125
|
+
if (colon <= 0) {
|
|
126
|
+
warnings.push(`Subtask "${subtask.id}": malformed substring-in-file criterion "${criterion}"`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const path = rest.slice(0, colon);
|
|
130
|
+
const needle = rest.slice(colon + 1);
|
|
131
|
+
const resolved = resolve(this.baseDir, path);
|
|
132
|
+
if (!existsSync(resolved)) {
|
|
133
|
+
const item = { passed: false, message: t("verify.file_not_found", { path }) };
|
|
134
|
+
details.push(item);
|
|
135
|
+
errors.push(`[${subtask.id}] ${item.message}`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const content = readFileSync(resolved, "utf-8");
|
|
139
|
+
const passed = content.includes(needle);
|
|
140
|
+
details.push({ passed, message: `substring "${needle}" in ${path}: ${passed}` });
|
|
141
|
+
if (!passed)
|
|
142
|
+
errors.push(`[${subtask.id}] "${needle}" not found in ${path}`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (kind === "command-exit-0") {
|
|
146
|
+
try {
|
|
147
|
+
await this.runAsync(rest, this.baseDir, 30_000);
|
|
148
|
+
details.push({ passed: true, message: `command ok: ${rest}` });
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
const item = {
|
|
152
|
+
passed: false,
|
|
153
|
+
message: `command failed: ${rest} (${String(e.message).slice(0, 200)})`,
|
|
154
|
+
};
|
|
155
|
+
details.push(item);
|
|
156
|
+
errors.push(`[${subtask.id}] ${item.message}`);
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
warnings.push(`Subtask "${subtask.id}": unknown criterion kind "${kind}" (LLM-only)`);
|
|
161
|
+
}
|
|
162
|
+
return { errors, warnings, details };
|
|
163
|
+
}
|
|
89
164
|
async verifyMoEManifest(plan, config, allToolTags) {
|
|
90
165
|
const errors = [];
|
|
91
166
|
const warnings = [];
|
|
@@ -108,6 +183,11 @@ export class StepVerifier {
|
|
|
108
183
|
errors.push(check.message);
|
|
109
184
|
}
|
|
110
185
|
}
|
|
186
|
+
// Machine-readable success_criteria (Plan 5.1) — checked before merge.
|
|
187
|
+
const criteria = await this.verifySubtaskCriteria(sub);
|
|
188
|
+
errors.push(...criteria.errors);
|
|
189
|
+
warnings.push(...criteria.warnings);
|
|
190
|
+
details.push(...criteria.details);
|
|
111
191
|
}
|
|
112
192
|
return {
|
|
113
193
|
success: errors.length === 0,
|
|
@@ -161,8 +241,15 @@ export class StepVerifier {
|
|
|
161
241
|
async validateSyntax(filePath) {
|
|
162
242
|
const ext = extname(filePath);
|
|
163
243
|
if (ext === ".ts" || ext === ".tsx" || ext === ".cts" || ext === ".mts") {
|
|
244
|
+
// Локальный tsc, а не npx: npx без локального пакета уходит в сеть и
|
|
245
|
+
// висит секундами. Нет tsc — пропускаем проверку (как и раньше «not
|
|
246
|
+
// found → true»), но мгновенно.
|
|
247
|
+
const tsc = resolveTscCommand(dirname(filePath));
|
|
248
|
+
if (!tsc)
|
|
249
|
+
return true;
|
|
164
250
|
try {
|
|
165
|
-
|
|
251
|
+
// Quote the path — commands run through a shell and break on spaces.
|
|
252
|
+
await this.runAsync(`${tsc} --noEmit --skipLibCheck "${filePath}"`, this.baseDir, 20000);
|
|
166
253
|
return true;
|
|
167
254
|
}
|
|
168
255
|
catch (err) {
|
|
@@ -180,7 +267,7 @@ export class StepVerifier {
|
|
|
180
267
|
}
|
|
181
268
|
if (ext === ".js" || ext === ".jsx" || ext === ".cjs" || ext === ".mjs") {
|
|
182
269
|
try {
|
|
183
|
-
await this.runAsync(`node --check ${filePath}`, this.baseDir, 5000);
|
|
270
|
+
await this.runAsync(`node --check "${filePath}"`, this.baseDir, 5000);
|
|
184
271
|
return true;
|
|
185
272
|
}
|
|
186
273
|
catch {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { t } from "../../i18n/index";
|
|
2
2
|
const MIN_CHARS = 1;
|
|
3
|
-
|
|
3
|
+
/** Both responses must be this long before repetition applies — a short
|
|
4
|
+
* honest answer ("Done." / "The build passes.") repeated twice is not
|
|
5
|
+
* degeneration and must not burn a hallucination retry. */
|
|
6
|
+
const MIN_REPEAT_CHARS = 120;
|
|
7
|
+
const MIN_REPEAT_WORDS = 20;
|
|
8
|
+
/** Word-overlap above this threshold counts as repetition (was 0.5). */
|
|
9
|
+
const REPEAT_OVERLAP_THRESHOLD = 0.8;
|
|
4
10
|
export class ConfidenceCheck {
|
|
5
11
|
previousResponse = "";
|
|
6
12
|
setPreviousResponse(response) {
|
|
@@ -14,25 +20,28 @@ export class ConfidenceCheck {
|
|
|
14
20
|
reason: t("hall.short_response"),
|
|
15
21
|
};
|
|
16
22
|
}
|
|
17
|
-
//
|
|
23
|
+
// Short replies are not flagged: a greeting, a terse acknowledgement or a
|
|
24
|
+
// one-line confirmation is a valid answer. Real truncation is caught by the
|
|
25
|
+
// provider's finish_reason ("length"), and emptiness by the retry above.
|
|
18
26
|
const wordCount = response.split(/\s+/).filter(Boolean).length;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
status: "warn",
|
|
23
|
-
kind: "short",
|
|
24
|
-
reason: t("hall.short_response"),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
// Language-agnostic: repetition detection via word overlap
|
|
27
|
+
// Language-agnostic: repetition detection via word overlap. Only flag when
|
|
28
|
+
// BOTH the current and the previous response are substantive — a short
|
|
29
|
+
// answer after a long one (or two short answers) is not a loop.
|
|
28
30
|
if (this.previousResponse) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
const prevWords = this.previousResponse.split(/\s+/).filter(Boolean).length;
|
|
32
|
+
const bothSubstantive = response.length >= MIN_REPEAT_CHARS &&
|
|
33
|
+
this.previousResponse.length >= MIN_REPEAT_CHARS &&
|
|
34
|
+
wordCount >= MIN_REPEAT_WORDS &&
|
|
35
|
+
prevWords >= MIN_REPEAT_WORDS;
|
|
36
|
+
if (bothSubstantive) {
|
|
37
|
+
const overlap = this.calculateOverlap(response, this.previousResponse);
|
|
38
|
+
if (overlap > REPEAT_OVERLAP_THRESHOLD) {
|
|
39
|
+
return {
|
|
40
|
+
status: "retry",
|
|
41
|
+
kind: "repetition",
|
|
42
|
+
reason: t("hall.repetitive", { pct: Math.round(overlap * 100) }),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
36
45
|
}
|
|
37
46
|
}
|
|
38
47
|
// Language-agnostic: very low word diversity (same words repeated)
|
|
@@ -56,11 +65,15 @@ export class ConfidenceCheck {
|
|
|
56
65
|
return { status: "pass" };
|
|
57
66
|
}
|
|
58
67
|
calculateOverlap(a, b) {
|
|
59
|
-
const wordsA = new Set(a.toLowerCase().split(/\s+/));
|
|
60
|
-
const wordsB = b.toLowerCase().split(/\s+/);
|
|
61
|
-
|
|
68
|
+
const wordsA = new Set(a.toLowerCase().split(/\s+/).filter(Boolean));
|
|
69
|
+
const wordsB = new Set(b.toLowerCase().split(/\s+/).filter(Boolean));
|
|
70
|
+
const smaller = Math.min(wordsA.size, wordsB.size);
|
|
71
|
+
if (smaller === 0)
|
|
62
72
|
return 0;
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
let matches = 0;
|
|
74
|
+
for (const w of wordsB)
|
|
75
|
+
if (wordsA.has(w))
|
|
76
|
+
matches++;
|
|
77
|
+
return matches / smaller;
|
|
65
78
|
}
|
|
66
79
|
}
|
|
@@ -7,6 +7,9 @@ export class ConsistencyCheck {
|
|
|
7
7
|
}
|
|
8
8
|
trackCreatedFile(path) {
|
|
9
9
|
this.createdFiles.add(path);
|
|
10
|
+
// A delete followed by a re-create un-deletes the file — otherwise
|
|
11
|
+
// wasFileDeleted() keeps reporting it as gone after delete+create.
|
|
12
|
+
this.deletedFiles.delete(path);
|
|
10
13
|
}
|
|
11
14
|
trackDeletedFile(path) {
|
|
12
15
|
this.deletedFiles.add(path);
|
|
@@ -7,11 +7,15 @@ export class HallucinationDetector {
|
|
|
7
7
|
confidence;
|
|
8
8
|
factual;
|
|
9
9
|
judge;
|
|
10
|
-
constructor(baseDir, llmProvider) {
|
|
10
|
+
constructor(baseDir, llmProvider, options) {
|
|
11
11
|
this.consistency = new ConsistencyCheck();
|
|
12
12
|
this.confidence = new ConfidenceCheck();
|
|
13
13
|
this.factual = baseDir ? new FactualCheck(baseDir) : null;
|
|
14
|
-
|
|
14
|
+
// The judge is opt-out: it only runs when a provider is supplied AND the
|
|
15
|
+
// caller did not explicitly disable it (the main agent disables it by
|
|
16
|
+
// default via config.hallucination.judge.enabled).
|
|
17
|
+
const judgeEnabled = options?.judgeEnabled ?? true;
|
|
18
|
+
this.judge = llmProvider && judgeEnabled ? new LLMJudge(llmProvider) : null;
|
|
15
19
|
}
|
|
16
20
|
getConsistencyCheck() {
|
|
17
21
|
return this.consistency;
|
|
@@ -28,7 +32,8 @@ export class HallucinationDetector {
|
|
|
28
32
|
if (confidenceResult.status === "retry" || confidenceResult.status === "block") {
|
|
29
33
|
return confidenceResult;
|
|
30
34
|
}
|
|
31
|
-
const
|
|
35
|
+
const deletedFiles = new Set(this.consistency.getDeletedFiles());
|
|
36
|
+
const factualResult = this.factual?.validate(response, deletedFiles);
|
|
32
37
|
if (factualResult && factualResult.status !== "pass") {
|
|
33
38
|
return factualResult;
|
|
34
39
|
}
|
|
@@ -32,12 +32,17 @@ export class FactualCheck {
|
|
|
32
32
|
for (const f of files)
|
|
33
33
|
this.knownFiles.add(f);
|
|
34
34
|
}
|
|
35
|
-
validate(response) {
|
|
35
|
+
validate(response, deletedFiles) {
|
|
36
36
|
const filePaths = this.extractFilePaths(response);
|
|
37
37
|
if (filePaths.length === 0)
|
|
38
38
|
return { status: "pass" };
|
|
39
|
+
const deletedBasenames = deletedFiles
|
|
40
|
+
? new Set([...deletedFiles].map((p) => p.split(/[/\\]/).pop() ?? p))
|
|
41
|
+
: undefined;
|
|
39
42
|
const nonExistent = [];
|
|
40
43
|
for (const fp of filePaths) {
|
|
44
|
+
if (deletedBasenames?.has(fp))
|
|
45
|
+
continue;
|
|
41
46
|
if (!this.pathExists(fp)) {
|
|
42
47
|
nonExistent.push(fp);
|
|
43
48
|
}
|
|
@@ -81,14 +86,28 @@ export class FactualCheck {
|
|
|
81
86
|
* non-word char and `.`), so dotfiles (.prettierrc.json, .env, …) are
|
|
82
87
|
* extracted as "prettierrc.json". Try the dot-prefixed basename too before
|
|
83
88
|
* declaring the file missing. "src/config.json" also yields
|
|
84
|
-
* "src/.config.json".
|
|
89
|
+
* "src/.config.json". For paths like "mma/index-cache.json", also try
|
|
90
|
+
* ".mma/index-cache.json" (dot-prefixed parent directories).
|
|
85
91
|
*/
|
|
86
92
|
dotfileVariants(fp) {
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
const variants = [fp];
|
|
94
|
+
const sep = fp.includes("\\") ? "\\" : "/";
|
|
95
|
+
const parts = fp.split(sep);
|
|
96
|
+
// Try dot-prefixing the basename (existing logic)
|
|
97
|
+
const base = parts[parts.length - 1];
|
|
98
|
+
if (!base.startsWith(".")) {
|
|
99
|
+
const withDotBase = [...parts.slice(0, -1), `.${base}`].join(sep);
|
|
100
|
+
variants.push(withDotBase);
|
|
101
|
+
}
|
|
102
|
+
// Try dot-prefixing each parent directory component
|
|
103
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
104
|
+
if (!parts[i].startsWith(".")) {
|
|
105
|
+
const dotted = [...parts.slice(0, i), `.${parts[i]}`, ...parts.slice(i + 1)].join(sep);
|
|
106
|
+
if (!variants.includes(dotted))
|
|
107
|
+
variants.push(dotted);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return variants;
|
|
92
111
|
}
|
|
93
112
|
bareNameExists(name) {
|
|
94
113
|
for (const cand of this.dotfileVariants(name)) {
|
|
@@ -56,17 +56,24 @@ Reply with ONLY valid JSON, no other text:
|
|
|
56
56
|
}
|
|
57
57
|
async askJudge(messages) {
|
|
58
58
|
let text = "";
|
|
59
|
+
const controller = new AbortController();
|
|
59
60
|
let timer;
|
|
60
61
|
const consume = (async () => {
|
|
61
|
-
for await (const chunk of this.provider.chat(messages, [])) {
|
|
62
|
+
for await (const chunk of this.provider.chat(messages, [], controller.signal)) {
|
|
62
63
|
if (chunk.type === "text" && chunk.content) {
|
|
63
64
|
text += chunk.content;
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
return text;
|
|
67
68
|
})();
|
|
69
|
+
// The timeout can settle the race while the background stream is still
|
|
70
|
+
// running; its later rejection must not surface as an unhandled rejection.
|
|
71
|
+
consume.catch(() => { });
|
|
68
72
|
const timeout = new Promise((_, reject) => {
|
|
69
|
-
timer = setTimeout(() =>
|
|
73
|
+
timer = setTimeout(() => {
|
|
74
|
+
controller.abort();
|
|
75
|
+
reject(new Error("LLM judge timed out"));
|
|
76
|
+
}, JUDGE_TIMEOUT_MS);
|
|
70
77
|
});
|
|
71
78
|
try {
|
|
72
79
|
return await Promise.race([consume, timeout]);
|
|
@@ -74,6 +81,9 @@ Reply with ONLY valid JSON, no other text:
|
|
|
74
81
|
finally {
|
|
75
82
|
if (timer)
|
|
76
83
|
clearTimeout(timer);
|
|
84
|
+
// Abort the background stream on the success path too, so a judge that
|
|
85
|
+
// resolved early never keeps streaming in the background.
|
|
86
|
+
controller.abort();
|
|
77
87
|
}
|
|
78
88
|
}
|
|
79
89
|
parseVerdict(text) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { t } from "../../i18n/index";
|
|
2
|
+
/**
|
|
3
|
+
* Shared implementation of the project-map command used by both `mma map`
|
|
4
|
+
* (CLI) and `/map` (REPL): summary (the exact text injected into the system
|
|
5
|
+
* prompt), refresh, and find.
|
|
6
|
+
*/
|
|
7
|
+
export async function runMapAction(module, action, query) {
|
|
8
|
+
const summary = async () => {
|
|
9
|
+
const result = await module.buildIndex();
|
|
10
|
+
if (!result)
|
|
11
|
+
return { ok: false, output: t("indexer.not_indexed") };
|
|
12
|
+
return { ok: true, output: module.getPromptMap() ?? t("indexer.not_indexed") };
|
|
13
|
+
};
|
|
14
|
+
switch (action) {
|
|
15
|
+
case "refresh": {
|
|
16
|
+
const result = await module.refresh();
|
|
17
|
+
if (!result)
|
|
18
|
+
return { ok: false, output: t("indexer.not_indexed") };
|
|
19
|
+
return {
|
|
20
|
+
ok: true,
|
|
21
|
+
output: `${t("map.refreshed")}\n${module.getPromptMap() ?? ""}`.trimEnd(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
case "find": {
|
|
25
|
+
const q = (query ?? "").trim();
|
|
26
|
+
if (!q)
|
|
27
|
+
return { ok: false, output: t("map.find_usage") };
|
|
28
|
+
await module.buildIndex();
|
|
29
|
+
return { ok: true, output: module.formatFind(q) };
|
|
30
|
+
}
|
|
31
|
+
case "summary":
|
|
32
|
+
default:
|
|
33
|
+
return summary();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { toForwardSlash } from "../../tools/path-utils";
|
|
2
|
+
import { isCodeLanguage } from "./symbols";
|
|
3
|
+
/** Lockfiles and generated manifests: no structural value in the map. */
|
|
4
|
+
const NOISE_BASENAMES = new Set([
|
|
5
|
+
"package-lock.json",
|
|
6
|
+
"bun.lock",
|
|
7
|
+
"bun.lockb",
|
|
8
|
+
"yarn.lock",
|
|
9
|
+
"pnpm-lock.yaml",
|
|
10
|
+
"cargo.lock",
|
|
11
|
+
"go.sum",
|
|
12
|
+
"composer.lock",
|
|
13
|
+
"poetry.lock",
|
|
14
|
+
]);
|
|
15
|
+
export function isNoiseFile(file) {
|
|
16
|
+
const base = toForwardSlash(file.path).split("/").pop()?.toLowerCase() ?? "";
|
|
17
|
+
return NOISE_BASENAMES.has(base);
|
|
18
|
+
}
|
|
19
|
+
function isTestFile(path) {
|
|
20
|
+
return (path.startsWith("test/") ||
|
|
21
|
+
path.startsWith("tests/") ||
|
|
22
|
+
path.includes("/test/") ||
|
|
23
|
+
path.includes("/tests/") ||
|
|
24
|
+
path.includes("__tests__/") ||
|
|
25
|
+
/\.(test|spec)\.[a-z0-9]+$/i.test(path));
|
|
26
|
+
}
|
|
27
|
+
/** Lower rank = more useful for understanding the project structure. */
|
|
28
|
+
function rank(file) {
|
|
29
|
+
const path = toForwardSlash(file.path).toLowerCase();
|
|
30
|
+
if (isTestFile(path))
|
|
31
|
+
return 1;
|
|
32
|
+
if (isCodeLanguage(file.language))
|
|
33
|
+
return 0;
|
|
34
|
+
return 2;
|
|
35
|
+
}
|
|
36
|
+
/** Top-level segment of a path (`src/core/x.ts` → `src`; root file → its name). */
|
|
37
|
+
function topDir(path) {
|
|
38
|
+
const normalized = toForwardSlash(path);
|
|
39
|
+
const slash = normalized.indexOf("/");
|
|
40
|
+
return slash >= 0 ? normalized.slice(0, slash) : normalized;
|
|
41
|
+
}
|
|
42
|
+
/** How many source-code files live under each top-level directory. */
|
|
43
|
+
function codeCountByTopDir(files) {
|
|
44
|
+
const counts = new Map();
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
if (isNoiseFile(file) || rank(file) !== 0)
|
|
47
|
+
continue;
|
|
48
|
+
const dir = topDir(file.path);
|
|
49
|
+
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
50
|
+
}
|
|
51
|
+
return counts;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Pick the files to list in the project-map prompt block.
|
|
55
|
+
*
|
|
56
|
+
* The index is built by an arbitrary `readdir` walk, so a naive
|
|
57
|
+
* `files.slice(0, N)` can end up listing only docs, config and tests while the
|
|
58
|
+
* actual source tree (`src/`) never appears — the model then sees a "project
|
|
59
|
+
* map" with no code in it. The selection:
|
|
60
|
+
* 1. drops lockfile noise,
|
|
61
|
+
* 2. ranks source before tests, tests before docs/config,
|
|
62
|
+
* 3. within a rank, puts directories with the most source files first (the
|
|
63
|
+
* densest code root is almost always the real source tree — no directory
|
|
64
|
+
* names hardcoded),
|
|
65
|
+
* 4. then shallower paths, then alphabetical — deterministic so the block
|
|
66
|
+
* does not change between turns and the prefix KV-cache survives.
|
|
67
|
+
*/
|
|
68
|
+
export function selectMapFiles(files, max) {
|
|
69
|
+
const codeByDir = codeCountByTopDir(files);
|
|
70
|
+
return files
|
|
71
|
+
.filter((file) => !isNoiseFile(file))
|
|
72
|
+
.sort((a, b) => {
|
|
73
|
+
const rankDiff = rank(a) - rank(b);
|
|
74
|
+
if (rankDiff !== 0)
|
|
75
|
+
return rankDiff;
|
|
76
|
+
const densityDiff = (codeByDir.get(topDir(b.path)) ?? 0) - (codeByDir.get(topDir(a.path)) ?? 0);
|
|
77
|
+
if (densityDiff !== 0)
|
|
78
|
+
return densityDiff;
|
|
79
|
+
const pathA = toForwardSlash(a.path);
|
|
80
|
+
const pathB = toForwardSlash(b.path);
|
|
81
|
+
const depthDiff = pathA.split("/").length - pathB.split("/").length;
|
|
82
|
+
if (depthDiff !== 0)
|
|
83
|
+
return depthDiff;
|
|
84
|
+
return pathA < pathB ? -1 : pathA > pathB ? 1 : 0;
|
|
85
|
+
})
|
|
86
|
+
.slice(0, max);
|
|
87
|
+
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { dirname } from "path";
|
|
2
2
|
import { Indexer } from "./walker";
|
|
3
3
|
import { IndexCache } from "./cache";
|
|
4
|
+
import { selectMapFiles } from "./map-select";
|
|
4
5
|
import { buildProjectProfileLine } from "./project-profile";
|
|
5
6
|
import { t } from "../../i18n/index";
|
|
7
|
+
import { toForwardSlash } from "../../tools/path-utils";
|
|
8
|
+
import { estimateTokens } from "../../llm/token-counter";
|
|
9
|
+
/** Max files listed in the project-map prompt block (code first; see map-select). */
|
|
10
|
+
const MAP_FILE_LIMIT = 80;
|
|
6
11
|
export class IndexerModule {
|
|
7
12
|
name = "indexer";
|
|
8
13
|
indexer;
|
|
@@ -47,6 +52,25 @@ export class IndexerModule {
|
|
|
47
52
|
return path.includes(q) || f.exports.some((e) => e.toLowerCase().includes(q));
|
|
48
53
|
});
|
|
49
54
|
}
|
|
55
|
+
/** Human/model-readable result of a `find` query (shared by the tool and `mma map`). */
|
|
56
|
+
formatFind(query) {
|
|
57
|
+
const matches = this.find(query);
|
|
58
|
+
if (matches.length === 0) {
|
|
59
|
+
return t("indexer.no_matches", { query });
|
|
60
|
+
}
|
|
61
|
+
const lines = matches
|
|
62
|
+
.map((f) => {
|
|
63
|
+
const path = toForwardSlash(f.path);
|
|
64
|
+
const exports = f.exports.length > 0 ? `: ${f.exports.join(", ")}` : "";
|
|
65
|
+
return `- ${path}${exports}`;
|
|
66
|
+
})
|
|
67
|
+
.join("\n");
|
|
68
|
+
return t("indexer.find_results", { count: matches.length, results: lines });
|
|
69
|
+
}
|
|
70
|
+
/** The exact project-map text injected into the system prompt (null if not indexed). */
|
|
71
|
+
getPromptMap() {
|
|
72
|
+
return this.index ? this.formatMap(this.index) : null;
|
|
73
|
+
}
|
|
50
74
|
watch() {
|
|
51
75
|
this.unwatch();
|
|
52
76
|
try {
|
|
@@ -80,6 +104,7 @@ export class IndexerModule {
|
|
|
80
104
|
priority: "normal",
|
|
81
105
|
essential: false,
|
|
82
106
|
estimatedTokens: this.estimateTokens(content),
|
|
107
|
+
kind: "project-map",
|
|
83
108
|
};
|
|
84
109
|
}
|
|
85
110
|
getToolDefinitions() {
|
|
@@ -106,13 +131,14 @@ export class IndexerModule {
|
|
|
106
131
|
.slice(0, 10)
|
|
107
132
|
.map(([dir, count]) => `${dir} (${count})`)
|
|
108
133
|
.join(", ") || "-";
|
|
109
|
-
const
|
|
110
|
-
|
|
134
|
+
const listed = selectMapFiles(result.files, MAP_FILE_LIMIT);
|
|
135
|
+
const fileLines = listed.map((f) => {
|
|
136
|
+
const path = toForwardSlash(f.path);
|
|
111
137
|
const exports = f.exports.length > 0 ? `: ${f.exports.join(", ")}` : "";
|
|
112
138
|
return `- ${path}${exports}`;
|
|
113
139
|
});
|
|
114
|
-
const more = result.files.length >
|
|
115
|
-
? `\n${t("indexer.and_more", { count: result.files.length -
|
|
140
|
+
const more = result.files.length > listed.length
|
|
141
|
+
? `\n${t("indexer.and_more", { count: result.files.length - listed.length })}`
|
|
116
142
|
: "";
|
|
117
143
|
const duplicates = this.findDuplicateBasenames(result);
|
|
118
144
|
return [
|
|
@@ -137,7 +163,7 @@ export class IndexerModule {
|
|
|
137
163
|
findDuplicateBasenames(result) {
|
|
138
164
|
const byDir = {};
|
|
139
165
|
for (const f of result.files) {
|
|
140
|
-
const path = f.path
|
|
166
|
+
const path = toForwardSlash(f.path);
|
|
141
167
|
const idx = path.lastIndexOf("/");
|
|
142
168
|
const dir = idx >= 0 ? path.slice(0, idx) : "";
|
|
143
169
|
const name = path.slice(idx + 1);
|
|
@@ -167,7 +193,7 @@ export class IndexerModule {
|
|
|
167
193
|
getDirectoryCounts(result) {
|
|
168
194
|
const counts = {};
|
|
169
195
|
for (const f of result.files) {
|
|
170
|
-
const normalized = f.path
|
|
196
|
+
const normalized = toForwardSlash(f.path);
|
|
171
197
|
const dir = dirname(normalized);
|
|
172
198
|
const key = dir === "." ? "(root)" : dir;
|
|
173
199
|
counts[key] = (counts[key] || 0) + 1;
|
|
@@ -175,7 +201,7 @@ export class IndexerModule {
|
|
|
175
201
|
return counts;
|
|
176
202
|
}
|
|
177
203
|
estimateTokens(content) {
|
|
178
|
-
return
|
|
204
|
+
return estimateTokens(content);
|
|
179
205
|
}
|
|
180
206
|
createProjectMapTool() {
|
|
181
207
|
return {
|
|
@@ -211,21 +237,7 @@ export class IndexerModule {
|
|
|
211
237
|
const query = String(args.query || "").toLowerCase();
|
|
212
238
|
if (!query)
|
|
213
239
|
return { success: false, output: t("tool.invalid_params") };
|
|
214
|
-
|
|
215
|
-
if (matches.length === 0) {
|
|
216
|
-
return this.makeResult(t("indexer.no_matches", { query }));
|
|
217
|
-
}
|
|
218
|
-
const lines = matches
|
|
219
|
-
.map((f) => {
|
|
220
|
-
const path = f.path.replace(/\\/g, "/");
|
|
221
|
-
const exports = f.exports.length > 0 ? `: ${f.exports.join(", ")}` : "";
|
|
222
|
-
return `- ${path}${exports}`;
|
|
223
|
-
})
|
|
224
|
-
.join("\n");
|
|
225
|
-
return this.makeResult(t("indexer.find_results", {
|
|
226
|
-
count: matches.length,
|
|
227
|
-
results: lines,
|
|
228
|
-
}));
|
|
240
|
+
return this.makeResult(this.formatFind(query));
|
|
229
241
|
}
|
|
230
242
|
if (!this.index) {
|
|
231
243
|
await this.buildIndex();
|