micro-models-agent 0.41.2 → 0.42.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/bin/mma.mjs +41 -41
- package/dist/cli/commands.js +19 -9
- package/dist/cli/completer.js +37 -36
- package/dist/cli/index.js +2 -2
- package/dist/cli/main.js +23 -48
- package/dist/cli/repl-commands.js +12 -40
- package/dist/cli/repl.js +87 -217
- package/dist/cli/security-commands.js +7 -5
- package/dist/cli/setup.js +26 -8
- package/dist/config/config.js +5 -52
- package/dist/config/defaults.js +5 -29
- package/dist/config/experts.js +1 -1
- package/dist/config/index.js +3 -3
- package/dist/config/security.js +10 -3
- package/dist/core/agent-moe.js +10 -2
- package/dist/core/agent.js +82 -273
- package/dist/core/bootstrap.js +13 -80
- package/dist/core/index.js +2 -2
- package/dist/core/prompt-builder.js +2 -23
- package/dist/core/session-logger.js +4 -46
- package/dist/i18n/en.json +2 -75
- package/dist/i18n/ru.json +1 -74
- package/dist/index.js +1 -1
- package/dist/llm/image-utils.js +5 -4
- package/dist/llm/index.js +4 -4
- package/dist/llm/model-loader.js +6 -6
- package/dist/llm/openai-compat.js +34 -40
- package/dist/llm/orchestrator.js +29 -33
- 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 +299 -55
- package/dist/migration/backup.js +13 -13
- package/dist/migration/detect.js +11 -11
- package/dist/migration/index.js +2 -2
- package/dist/modules/browser/actions.js +4 -34
- package/dist/modules/browser/bridge-server.mjs +202 -202
- package/dist/modules/browser/cookie-store.js +6 -6
- package/dist/modules/browser/index.js +5 -7
- package/dist/modules/browser/module.js +7 -8
- package/dist/modules/browser/session.js +84 -87
- package/dist/modules/browser/snapshot.js +58 -92
- package/dist/modules/browser/types.js +1 -4
- package/dist/modules/certification/cli.js +4 -2
- package/dist/modules/certification/fact-checker.js +3 -1
- package/dist/modules/certification/loader.js +9 -3
- package/dist/modules/certification/runner.js +4 -1
- package/dist/modules/context/index.js +1 -1
- package/dist/modules/context/manager.js +86 -160
- package/dist/modules/execution/auditor.js +25 -177
- package/dist/modules/execution/module.js +544 -201
- package/dist/modules/execution/moe-executor.js +0 -25
- package/dist/modules/execution/plan-store.js +3 -1
- package/dist/modules/execution/plan-validator.js +10 -10
- package/dist/modules/execution/planner.js +1 -6
- package/dist/modules/execution/stuck-detector.js +10 -173
- package/dist/modules/execution/verifier.js +42 -86
- package/dist/modules/hallucination/confidence.js +1 -8
- package/dist/modules/hallucination/detector.js +5 -2
- package/dist/modules/hallucination/factual.js +64 -3
- package/dist/modules/hallucination/index.js +1 -1
- package/dist/modules/hallucination/js-identifiers.js +0 -190
- package/dist/modules/hallucination/llm-judge.js +3 -1
- package/dist/modules/indexer/cache.js +7 -9
- package/dist/modules/indexer/index.js +3 -3
- package/dist/modules/indexer/module.js +42 -95
- package/dist/modules/indexer/walker.js +17 -17
- package/dist/modules/lsp/client.js +31 -74
- package/dist/modules/lsp/config.js +33 -87
- package/dist/modules/lsp/index.js +3 -3
- package/dist/modules/lsp/module.js +21 -185
- package/dist/modules/mcp/module.js +6 -2
- package/dist/modules/memory/index.js +1 -1
- package/dist/modules/memory/module.js +23 -71
- package/dist/modules/memory/search.js +9 -11
- 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 +14 -17
- package/dist/modules/pipelines/template.js +1 -1
- package/dist/modules/plugins/builtin/lint-on-write.js +16 -21
- package/dist/modules/plugins/builtin/notify.js +2 -3
- package/dist/modules/plugins/index.js +1 -1
- package/dist/modules/plugins/loader.js +17 -59
- package/dist/modules/plugins/manager.js +17 -73
- package/dist/modules/processes/index.js +1 -1
- package/dist/modules/processes/registry.js +46 -135
- package/dist/modules/registry.js +2 -4
- package/dist/modules/security/audit-notifier.js +39 -39
- package/dist/modules/security/command-validator.js +8 -2
- package/dist/modules/security/data-sanitizer.js +9 -1
- package/dist/modules/security/encryption.js +56 -58
- package/dist/modules/security/network-validator.js +9 -1
- package/dist/modules/security/path-validator.js +3 -1
- package/dist/modules/security/security-policies.js +19 -3
- 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 +9 -3
- package/dist/modules/skills/module.js +2 -1
- package/dist/modules/updater/checker.js +6 -70
- package/dist/modules/updater/index.js +1 -2
- 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 +19 -178
- package/dist/tools/browser.js +29 -46
- package/dist/tools/executor.js +5 -4
- package/dist/tools/file-info.js +12 -13
- package/dist/tools/filter-tools.js +2 -9
- package/dist/tools/glob-tool.js +11 -11
- package/dist/tools/grep-tool.js +3 -1
- package/dist/tools/index.js +2 -13
- package/dist/tools/list-dir.js +17 -18
- package/dist/tools/load-skill.js +3 -1
- 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 +22 -20
- package/dist/tools/process-log.js +18 -22
- package/dist/tools/question.js +3 -1
- package/dist/tools/read-file.js +2 -10
- package/dist/tools/recall.js +37 -44
- package/dist/tools/registry.js +4 -15
- package/dist/tools/remember.js +29 -29
- package/dist/tools/scope-check.js +9 -9
- package/dist/tools/subagent.js +9 -54
- 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 +3 -1
- package/dist/ui/box.js +5 -1
- package/dist/ui/index.js +6 -6
- package/dist/ui/md-formatter.js +33 -33
- package/dist/ui/output.js +5 -5
- package/dist/ui/renderer.js +10 -15
- package/dist/ui/table.js +1 -1
- package/package.json +48 -48
- package/dist/cli/plugin-commands.js +0 -36
- package/dist/cli/run-result.js +0 -22
- package/dist/core/version.js +0 -24
- package/dist/modules/artifacts/store.js +0 -61
- package/dist/modules/browser/bridge-client.js +0 -199
- package/dist/modules/browser/bridge-path.js +0 -10
- package/dist/modules/browser/driver.js +0 -136
- package/dist/modules/context/chunk-query.js +0 -100
- package/dist/modules/context/fact-extractor.js +0 -162
- package/dist/modules/context/history.js +0 -15
- package/dist/modules/execution/audit-runners.js +0 -152
- package/dist/modules/execution/execution-plugin.js +0 -272
- package/dist/modules/execution/plan-tool.js +0 -508
- package/dist/modules/execution/windows-commands.js +0 -41
- package/dist/modules/indexer/project-profile.js +0 -183
- package/dist/modules/lsp/check-tool.js +0 -58
- package/dist/modules/lsp/command.js +0 -60
- package/dist/modules/lsp/probe.js +0 -76
- package/dist/modules/lsp/project-root.js +0 -32
- package/dist/modules/lsp/startup-check.js +0 -141
- package/dist/modules/processes/detect.js +0 -34
- package/dist/modules/skills/matcher.js +0 -27
- package/dist/modules/updater/module.js +0 -116
- package/dist/tools/chunk-query.js +0 -99
- package/dist/tools/download-file.js +0 -116
- package/dist/tools/enable-tools.js +0 -58
- package/dist/tools/hidden-tools-block.js +0 -37
- package/dist/ui/line-editor.js +0 -703
- package/dist/ui/line-math.js +0 -69
- package/dist/ui/plan-view.js +0 -103
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { getMessageText } from "../../llm/provider";
|
|
2
|
-
import { FactExtractor } from "./fact-extractor";
|
|
3
2
|
const COMPACTION_INTERVAL = 15;
|
|
4
3
|
const KEEP_LAST_N = 6;
|
|
5
4
|
function summarizeArgs(args) {
|
|
@@ -20,28 +19,15 @@ function truncate(s, max) {
|
|
|
20
19
|
return s;
|
|
21
20
|
return s.slice(0, max - 3) + "...";
|
|
22
21
|
}
|
|
23
|
-
function looksLikeErrorPaste(text) {
|
|
24
|
-
// Error markers.
|
|
25
|
-
if (/(?:^|\s)ERROR|Error:|error TS\d+|Transform failed|\[plugin:|SyntaxError|Cannot find|Uncaught|exception/i.test(text)) {
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
// Structural signals of pasted compiler/runtime output: source frames
|
|
29
|
-
// ("270| }_s(...)"), path:line:col references, and stack-trace lines.
|
|
30
|
-
if (/(?:^|\n)\s*\d+\s*\|/.test(text))
|
|
31
|
-
return true;
|
|
32
|
-
if (/[\w./\\-]+\.[a-z]{1,6}:\d+:\d+/.test(text))
|
|
33
|
-
return true;
|
|
34
|
-
if (/(?:^|\n)\s*at\s+\S+/.test(text))
|
|
35
|
-
return true;
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
22
|
export function extractTriedAndFailed(messages) {
|
|
39
23
|
const failures = new Map();
|
|
40
24
|
for (const msg of messages) {
|
|
41
25
|
if (msg.role === "tool" && msg.name && msg.success === false) {
|
|
42
26
|
const key = `${msg.name}:${summarizeArgs(msg.arguments)}`;
|
|
43
27
|
const existing = failures.get(key);
|
|
44
|
-
const errorText = truncate(typeof msg.content === "string"
|
|
28
|
+
const errorText = truncate(typeof msg.content === "string"
|
|
29
|
+
? msg.content
|
|
30
|
+
: getMessageText(msg.content), 100);
|
|
45
31
|
if (existing) {
|
|
46
32
|
existing.count++;
|
|
47
33
|
}
|
|
@@ -66,38 +52,20 @@ export class ContextManager {
|
|
|
66
52
|
peakTokens = 0;
|
|
67
53
|
budget;
|
|
68
54
|
compactionThreshold;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
sessionMission = "";
|
|
74
|
-
/** Latest user message that looks like a pasted error/log (never a task). */
|
|
75
|
-
lastUserFeedback = "";
|
|
55
|
+
fileFacts = [];
|
|
56
|
+
decisionFacts = [];
|
|
57
|
+
errorFacts = [];
|
|
58
|
+
static MAX_FACTS = 20;
|
|
76
59
|
tokenCounter;
|
|
77
60
|
pendingImageParts = [];
|
|
78
61
|
toolTokens = 0;
|
|
79
62
|
onCompact = null;
|
|
80
|
-
/**
|
|
81
|
-
* Optional short line appended to the compaction summary so the model knows
|
|
82
|
-
* an active plan exists with progress. Wired from bootstrap to the execution
|
|
83
|
-
* module — without it, a 9B model re-creates a plan from scratch right after
|
|
84
|
-
* compaction (observed: ses_msvuao0h — plan_e07pb2 (1/6) was discarded for a
|
|
85
|
-
* fresh plan_xzq9xe the same iteration the old plan was still visible).
|
|
86
|
-
*/
|
|
87
|
-
planSummaryProvider = null;
|
|
88
63
|
constructor(contextWindow, contextBudget, tokenCounter) {
|
|
89
64
|
this.contextWindow = contextWindow;
|
|
90
65
|
this.compactionThreshold = contextBudget?.compactionThreshold ?? 0.75;
|
|
91
66
|
this.budget = this.calculateBudget(contextWindow, contextBudget);
|
|
92
67
|
this.tokenCounter = tokenCounter ?? null;
|
|
93
68
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Provide a short "current plan" line for the compaction summary, or null
|
|
96
|
-
* when no plan is active. Injected once at bootstrap (not per-turn state).
|
|
97
|
-
*/
|
|
98
|
-
setPlanSummaryProvider(fn) {
|
|
99
|
-
this.planSummaryProvider = fn;
|
|
100
|
-
}
|
|
101
69
|
calculateBudget(window, contextBudget) {
|
|
102
70
|
if (contextBudget) {
|
|
103
71
|
const systemPrompt = Math.floor(window * contextBudget.systemPrompt);
|
|
@@ -124,67 +92,27 @@ export class ContextManager {
|
|
|
124
92
|
noteIteration() {
|
|
125
93
|
this.iterationsSinceCompaction++;
|
|
126
94
|
}
|
|
127
|
-
/**
|
|
128
|
-
* Start a fresh user turn. The 15-iteration compaction interval must not
|
|
129
|
-
* leak across user messages — otherwise a turn that ends at iteration 14
|
|
130
|
-
* forces a compaction on the very first iteration of the next user message
|
|
131
|
-
* (observed: compaction fired on iteration 8 of a new turn, deleting the
|
|
132
|
-
* user's freshly-sent task alongside 40+ old turns). Facts are kept.
|
|
133
|
-
*/
|
|
134
|
-
resetUserTurn() {
|
|
135
|
-
this.iterationsSinceCompaction = 0;
|
|
136
|
-
// A new user message begins a new turn — drop the previous turn's task
|
|
137
|
-
// so the fresh instruction becomes the one preserved at compaction. The
|
|
138
|
-
// session mission survives: an error/log paste in the new turn must not
|
|
139
|
-
// erase the original goal.
|
|
140
|
-
this.lastUserTask = "";
|
|
141
|
-
this.lastUserFeedback = "";
|
|
142
|
-
}
|
|
143
95
|
getQuality() {
|
|
144
96
|
const usedTokens = this.getEstimatedTokens();
|
|
145
97
|
const tokenLoad = Math.max(0, 1 - usedTokens / this.budget.history);
|
|
146
|
-
|
|
147
|
-
// penalty is CAPPED so cumulative compactions can never permanently pin
|
|
148
|
-
// quality below the forced-compaction trigger (40%). Before this cap, a
|
|
149
|
-
// session with ~7+ compactions clamped the term to 0 forever; combined
|
|
150
|
-
// with heavy token load + error density, quality stuck below 40 and a
|
|
151
|
-
// compaction fired on EVERY iteration (observed: 56 compactions in 28 min).
|
|
152
|
-
const COMPACTION_PENALTY_FLOOR = 0.5;
|
|
153
|
-
const compactionLoss = Math.max(COMPACTION_PENALTY_FLOOR, 1 - this.compactionCount * 0.15);
|
|
98
|
+
const compactionLoss = Math.max(0, 1 - this.compactionCount * 0.15);
|
|
154
99
|
const msgCount = this.messages.length || 1;
|
|
155
|
-
const errorDensity = Math.max(0, 1 - Math.min(1, this.
|
|
100
|
+
const errorDensity = Math.max(0, 1 - Math.min(1, this.errorFacts.length / msgCount));
|
|
156
101
|
const freshness = Math.max(0, 1 - this.iterationsSinceCompaction / COMPACTION_INTERVAL);
|
|
157
|
-
const score = tokenLoad * 0.4 +
|
|
102
|
+
const score = tokenLoad * 0.4 +
|
|
103
|
+
compactionLoss * 0.25 +
|
|
104
|
+
errorDensity * 0.2 +
|
|
105
|
+
freshness * 0.15;
|
|
158
106
|
return Math.round(Math.min(100, Math.max(0, score * 100)));
|
|
159
107
|
}
|
|
160
108
|
addMessage(msg) {
|
|
161
|
-
// Track the task the agent is working on so it survives compaction. The
|
|
162
|
-
// user's real instruction is the FIRST non-system-summary user message of
|
|
163
|
-
// the turn; everything after it (tool results, retry nudges) are injected
|
|
164
|
-
// `user` envelopes that must not overwrite it (observed: after compaction
|
|
165
|
-
// the model had forgotten "удали /bicycles, почини стили" and re-read
|
|
166
|
-
// files aimlessly for 40+ iterations).
|
|
167
|
-
if (msg.role === "user") {
|
|
168
|
-
const text = typeof msg.content === "string" ? msg.content : getMessageText(msg.content);
|
|
169
|
-
if (text.trim() && !text.startsWith("<system-summary>") && !text.includes("[Compressed:")) {
|
|
170
|
-
if (looksLikeErrorPaste(text) && (this.lastUserTask || this.sessionMission)) {
|
|
171
|
-
this.lastUserFeedback = text.trim().slice(0, 400);
|
|
172
|
-
}
|
|
173
|
-
else if (!this.lastUserTask && !this.lastUserFeedback) {
|
|
174
|
-
// No new task inside a turn that already captured an error/log paste:
|
|
175
|
-
// the paste's follow-up messages are tool-result continuations, not a
|
|
176
|
-
// fresh instruction, so the session mission must win at compaction.
|
|
177
|
-
this.lastUserTask = text.trim();
|
|
178
|
-
if (!this.sessionMission)
|
|
179
|
-
this.sessionMission = text.trim();
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
109
|
// Auto-attach pending images to the next user message
|
|
184
110
|
if (msg.role === "user" && this.pendingImageParts.length > 0) {
|
|
185
111
|
const textPart = {
|
|
186
112
|
type: "text",
|
|
187
|
-
text: typeof msg.content === "string"
|
|
113
|
+
text: typeof msg.content === "string"
|
|
114
|
+
? msg.content
|
|
115
|
+
: getMessageText(msg.content),
|
|
188
116
|
};
|
|
189
117
|
msg = {
|
|
190
118
|
...msg,
|
|
@@ -278,53 +206,27 @@ export class ContextManager {
|
|
|
278
206
|
return totalTokens > this.budget.history * this.compactionThreshold;
|
|
279
207
|
}
|
|
280
208
|
compact() {
|
|
281
|
-
// Capture the pre-compaction state BEFORE any counters reset or rise —
|
|
282
|
-
// qualityBefore must reflect the context the model actually saw.
|
|
283
|
-
const tokensBefore = this.getEstimatedTokens();
|
|
284
|
-
const qualityBefore = this.getQuality();
|
|
285
|
-
const messagesBefore = this.messages.length;
|
|
286
209
|
// Reset the counter even when nothing to compact — otherwise
|
|
287
210
|
// needsCompaction() returns true forever after 15 iterations with few messages.
|
|
288
211
|
this.iterationsSinceCompaction = 0;
|
|
289
212
|
if (this.messages.length <= KEEP_LAST_N * 2)
|
|
290
|
-
return
|
|
213
|
+
return;
|
|
291
214
|
this.compactionCount++;
|
|
292
215
|
const cutoff = this.messages.length - KEEP_LAST_N * 2;
|
|
293
216
|
const oldTurns = this.messages.slice(0, cutoff);
|
|
294
217
|
const recentTurns = this.messages.slice(cutoff);
|
|
295
|
-
this.
|
|
218
|
+
this.extractFacts(oldTurns);
|
|
296
219
|
const parts = [];
|
|
297
220
|
parts.push(`[Compressed: ${oldTurns.length} old turns removed]`);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
parts.push(`[User feedback: ${this.lastUserFeedback}]`);
|
|
221
|
+
if (this.fileFacts.length > 0) {
|
|
222
|
+
parts.push(`[Files: ${this.fileFacts.slice(-15).join("; ")}]`);
|
|
223
|
+
}
|
|
224
|
+
if (this.decisionFacts.length > 0) {
|
|
225
|
+
parts.push(`[Decisions: ${this.decisionFacts.slice(-5).join("; ")}]`);
|
|
226
|
+
}
|
|
227
|
+
if (this.errorFacts.length > 0) {
|
|
228
|
+
parts.push(`[Errors: ${this.errorFacts.slice(-3).join("; ")}]`);
|
|
307
229
|
}
|
|
308
|
-
// Orient the model after compaction: an active plan must not be re-created
|
|
309
|
-
// from scratch (see planSummaryProvider doc).
|
|
310
|
-
const planLine = this.planSummaryProvider?.();
|
|
311
|
-
if (planLine)
|
|
312
|
-
parts.push(`[Plan: ${planLine}]`);
|
|
313
|
-
const filesLine = this.facts.filesLine();
|
|
314
|
-
if (filesLine)
|
|
315
|
-
parts.push(filesLine);
|
|
316
|
-
const deletedLine = this.facts.deletedLine();
|
|
317
|
-
if (deletedLine)
|
|
318
|
-
parts.push(deletedLine);
|
|
319
|
-
const readLine = this.facts.readLine();
|
|
320
|
-
if (readLine)
|
|
321
|
-
parts.push(readLine);
|
|
322
|
-
const decisionsLine = this.facts.decisionsLine();
|
|
323
|
-
if (decisionsLine)
|
|
324
|
-
parts.push(decisionsLine);
|
|
325
|
-
const errorsLine = this.facts.errorsLine();
|
|
326
|
-
if (errorsLine)
|
|
327
|
-
parts.push(errorsLine);
|
|
328
230
|
const triedAndFailed = extractTriedAndFailed(oldTurns);
|
|
329
231
|
if (triedAndFailed.length > 0) {
|
|
330
232
|
const lines = triedAndFailed.map((t) => `- ${t.tool}(${t.args}): ${t.error} (failed ${t.count}x)`);
|
|
@@ -343,38 +245,69 @@ export class ContextManager {
|
|
|
343
245
|
const text = getMessageText(m.content);
|
|
344
246
|
return !text.startsWith("<system-summary>");
|
|
345
247
|
});
|
|
346
|
-
this.messages = [
|
|
248
|
+
this.messages = [
|
|
249
|
+
...(firstSystem ? [firstSystem] : []),
|
|
250
|
+
summary,
|
|
251
|
+
...freshRecent,
|
|
252
|
+
];
|
|
347
253
|
this.iterationsSinceCompaction = 0;
|
|
348
254
|
if (this.onCompact) {
|
|
349
255
|
this.onCompact(summary);
|
|
350
256
|
}
|
|
351
|
-
return {
|
|
352
|
-
removedTurns: oldTurns.length,
|
|
353
|
-
keptTurns: freshRecent.length,
|
|
354
|
-
tokensBefore,
|
|
355
|
-
tokensAfter: this.getEstimatedTokens(),
|
|
356
|
-
qualityBefore,
|
|
357
|
-
qualityAfter: this.getQuality(),
|
|
358
|
-
messagesBefore,
|
|
359
|
-
messagesAfter: this.messages.length,
|
|
360
|
-
summary: this.compactedBlock ?? "",
|
|
361
|
-
};
|
|
362
257
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
258
|
+
extractFacts(turns) {
|
|
259
|
+
const filePatterns = [
|
|
260
|
+
/(?:Created|Updated|Written|Deleted|Moved) ([\w./\\-]+\.[a-z]+)/gi,
|
|
261
|
+
/Файл (?:создан|обновлён|записан|удалён|перемещён):? ([\w./\\-]+\.[a-z]+)/gi,
|
|
262
|
+
/file (?:created|updated|written|deleted|moved):? ([\w./\\-]+\.[a-z]+)/gi,
|
|
263
|
+
];
|
|
264
|
+
const newFiles = [];
|
|
265
|
+
const newDecisions = [];
|
|
266
|
+
const newErrors = [];
|
|
267
|
+
for (const msg of turns) {
|
|
268
|
+
const content = getMessageText(msg.content);
|
|
269
|
+
if (msg.role === "tool") {
|
|
270
|
+
for (const pattern of filePatterns) {
|
|
271
|
+
for (const match of content.matchAll(pattern)) {
|
|
272
|
+
newFiles.push(match[1]);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (content.includes("Plan:") && content.includes("[")) {
|
|
276
|
+
const planLine = content
|
|
277
|
+
.split("\n")
|
|
278
|
+
.find((line) => line.includes("Plan:"));
|
|
279
|
+
if (planLine)
|
|
280
|
+
newDecisions.push(planLine.trim());
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (msg.role === "assistant") {
|
|
284
|
+
if (content.includes("decided:") || content.includes("decision:")) {
|
|
285
|
+
const line = content
|
|
286
|
+
.split("\n")
|
|
287
|
+
.find((l) => l.includes("decided:") || l.includes("decision:"));
|
|
288
|
+
if (line)
|
|
289
|
+
newDecisions.push(line.trim().slice(0, 200));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (content.includes("Error:") ||
|
|
293
|
+
content.includes("failed") ||
|
|
294
|
+
content.includes("Ошибка:") ||
|
|
295
|
+
content.includes("не удалось")) {
|
|
296
|
+
const line = content
|
|
297
|
+
.split("\n")
|
|
298
|
+
.find((l) => l.includes("Error:") ||
|
|
299
|
+
l.includes("failed") ||
|
|
300
|
+
l.includes("Ошибка:") ||
|
|
301
|
+
l.includes("не удалось"));
|
|
302
|
+
if (line)
|
|
303
|
+
newErrors.push(line.trim().slice(0, 250));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Deduplicate and cap facts to prevent unbounded growth
|
|
307
|
+
const dedup = (arr) => [...new Set(arr)];
|
|
308
|
+
this.fileFacts = dedup([...this.fileFacts, ...newFiles]).slice(-ContextManager.MAX_FACTS);
|
|
309
|
+
this.decisionFacts = dedup([...this.decisionFacts, ...newDecisions]).slice(-ContextManager.MAX_FACTS);
|
|
310
|
+
this.errorFacts = dedup([...this.errorFacts, ...newErrors]).slice(-ContextManager.MAX_FACTS);
|
|
378
311
|
}
|
|
379
312
|
/**
|
|
380
313
|
* Replace the system prompt in place (keeps it first) or prepend a new one.
|
|
@@ -392,23 +325,16 @@ export class ContextManager {
|
|
|
392
325
|
getActiveHistory() {
|
|
393
326
|
return [...this.messages];
|
|
394
327
|
}
|
|
395
|
-
/**
|
|
396
|
-
* Reset everything that belongs to the current conversation turn.
|
|
397
|
-
* Fact lists (FactExtractor) intentionally survive clear() — they are
|
|
398
|
-
* session-scoped compaction state, not per-turn.
|
|
399
|
-
*/
|
|
400
328
|
clear() {
|
|
401
329
|
this.messages = [];
|
|
402
330
|
this.compactedBlock = null;
|
|
403
331
|
this.iterationsSinceCompaction = 0;
|
|
404
332
|
this.compactionCount = 0;
|
|
405
333
|
this.peakTokens = 0;
|
|
406
|
-
this.lastUserTask = "";
|
|
407
|
-
this.lastUserFeedback = "";
|
|
408
|
-
this.sessionMission = "";
|
|
409
334
|
}
|
|
410
335
|
getEstimatedTokens() {
|
|
411
|
-
return (this.messages.reduce((sum, m) => sum + this.estimateMessageTokens(m), 0) +
|
|
336
|
+
return (this.messages.reduce((sum, m) => sum + this.estimateMessageTokens(m), 0) +
|
|
337
|
+
this.toolTokens);
|
|
412
338
|
}
|
|
413
339
|
setToolTokens(tokens) {
|
|
414
340
|
this.toolTokens = tokens;
|
|
@@ -1,218 +1,66 @@
|
|
|
1
|
-
import { existsSync
|
|
2
|
-
import { resolve
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
3
|
import { t } from "../../i18n/index";
|
|
4
|
-
import {
|
|
5
|
-
import { SKIP_DIRS, findTestFile, findTypecheckRoot, hasTestStep, runTests, runTypecheck, } from "./audit-runners";
|
|
6
|
-
export { parseTypecheckErrors } from "./audit-runners";
|
|
4
|
+
import { isJsMemberAccess } from "../hallucination/js-identifiers";
|
|
7
5
|
const MASS_EDIT_THRESHOLD = 10;
|
|
8
|
-
/** Subtree walk depth cap for file resolution. */
|
|
9
|
-
const RESOLVE_MAX_DEPTH = 8;
|
|
10
|
-
/**
|
|
11
|
-
* Resolve a plan-step file token to an existing file.
|
|
12
|
-
*
|
|
13
|
-
* A step description frequently names a file by its bare basename
|
|
14
|
-
* ("App.tsx") or a partially-prefixed path ("src/App.tsx") while the real
|
|
15
|
-
* file lives in a nested project directory relative to the agent baseDir
|
|
16
|
-
* (e.g. `bicycle-shop/src/App.tsx`). Naively resolving against baseDir
|
|
17
|
-
* reports the file as missing — observed in session ses_mssl5izw where the
|
|
18
|
-
* final audit gate rejected a fully-completed task with "1 files missing"
|
|
19
|
-
* forever, sending the model into a read/retry loop.
|
|
20
|
-
*
|
|
21
|
-
* Strategy: exact resolve first, then a bounded subtree walk matching either
|
|
22
|
-
* the exact basename or a path suffix (so both "App.tsx" and
|
|
23
|
-
* "src/App.tsx" find "bicycle-shop/src/App.tsx").
|
|
24
|
-
*/
|
|
25
|
-
export function findExistingFile(baseDir, filePath) {
|
|
26
|
-
const direct = resolve(baseDir, filePath);
|
|
27
|
-
if (existsSync(direct))
|
|
28
|
-
return direct;
|
|
29
|
-
const name = basename(filePath).toLowerCase();
|
|
30
|
-
const suffix = filePath.replace(/\\/g, "/").toLowerCase();
|
|
31
|
-
let found = null;
|
|
32
|
-
const walk = (dir, depth) => {
|
|
33
|
-
if (found || depth > RESOLVE_MAX_DEPTH)
|
|
34
|
-
return;
|
|
35
|
-
let entries;
|
|
36
|
-
try {
|
|
37
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
for (const e of entries) {
|
|
43
|
-
if (found)
|
|
44
|
-
return;
|
|
45
|
-
const full = join(dir, e.name);
|
|
46
|
-
if (e.isDirectory()) {
|
|
47
|
-
if (SKIP_DIRS.has(e.name))
|
|
48
|
-
continue;
|
|
49
|
-
walk(full, depth + 1);
|
|
50
|
-
}
|
|
51
|
-
else if (e.name.toLowerCase() === name ||
|
|
52
|
-
full
|
|
53
|
-
.replace(/\\/g, "/")
|
|
54
|
-
.toLowerCase()
|
|
55
|
-
.endsWith("/" + suffix)) {
|
|
56
|
-
found = full;
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
walk(baseDir, 0);
|
|
62
|
-
return found;
|
|
63
|
-
}
|
|
64
6
|
export class Auditor {
|
|
65
7
|
baseDir;
|
|
66
8
|
constructor(baseDir) {
|
|
67
9
|
this.baseDir = baseDir;
|
|
68
10
|
}
|
|
69
11
|
async audit(plan) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// first so a hostname / https://host/repo.git in a step description
|
|
81
|
-
// can never be reported as a missing file (observed: a git-remote
|
|
82
|
-
// plan flagged `whom-pointing-jewel-maine.trycloudflare.com`).
|
|
83
|
-
const createFiles = extractFileLikeTokens(stripUrls(createText));
|
|
84
|
-
const deleteFiles = extractFileLikeTokens(stripUrls(deleteText));
|
|
12
|
+
const allStepText = plan.steps
|
|
13
|
+
.map((s) => s.description.replace(/\([^)]*\)/g, " "))
|
|
14
|
+
.join(" ");
|
|
15
|
+
const fileMatches = allStepText.match(/\b[\w./-]+\.[a-z]+/gi) || [];
|
|
16
|
+
// process.env / console.log in step descriptions are JS member access,
|
|
17
|
+
// not files — drop them before existence checks (false "missing file"
|
|
18
|
+
// warnings).
|
|
19
|
+
const uniqueFiles = [
|
|
20
|
+
...new Set(fileMatches.filter((f) => !isJsMemberAccess(f))),
|
|
21
|
+
];
|
|
85
22
|
const missingFiles = [];
|
|
86
23
|
const existingFiles = [];
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// Keep the resolved absolute path: findTypecheckRoot walks up
|
|
92
|
-
// from it to the nearest tsconfig, and a relative token would
|
|
93
|
-
// resolve against the process cwd instead of baseDir.
|
|
94
|
-
existingFiles.push(resolved);
|
|
24
|
+
for (const filePath of uniqueFiles) {
|
|
25
|
+
const resolved = resolve(this.baseDir, filePath);
|
|
26
|
+
if (existsSync(resolved)) {
|
|
27
|
+
existingFiles.push(filePath);
|
|
95
28
|
}
|
|
96
29
|
else {
|
|
97
30
|
missingFiles.push(filePath);
|
|
98
31
|
}
|
|
99
32
|
}
|
|
100
|
-
// A done delete-kind step requires its named files to be GONE.
|
|
101
|
-
for (const filePath of deleteFiles) {
|
|
102
|
-
if (findExistingFile(this.baseDir, filePath)) {
|
|
103
|
-
leftoverFiles.push(filePath);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// Real test run: when the plan includes a verification step, do not
|
|
107
|
-
// trust the model's word (or exit codes) — actually execute the tests
|
|
108
|
-
// and check the runner summary. This catches the "all 7 passing ✅"
|
|
109
|
-
// final answers that hide 3 failing tests.
|
|
110
|
-
let testRun = null;
|
|
111
|
-
if (hasTestStep(plan) && findTestFile(this.baseDir)) {
|
|
112
|
-
try {
|
|
113
|
-
testRun = await runTests(this.baseDir);
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
testRun = null; // infra failure — don't block on it
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
// Real typecheck: the audit must catch "the build is broken" even when
|
|
120
|
-
// every file exists (observed: deleting .js files left the .tsx sources
|
|
121
|
-
// with 11 tsc errors and the agent still declared success). Runs only
|
|
122
|
-
// when a tsconfig.json is reachable from baseDir or the resolved step
|
|
123
|
-
// files AND the file checks already passed — an audit that is doomed by
|
|
124
|
-
// missing/leftover files must not pay a 90s tsc spawn. Inconclusive
|
|
125
|
-
// results never block.
|
|
126
|
-
let typecheckError = null;
|
|
127
|
-
if (missingFiles.length === 0 && leftoverFiles.length === 0) {
|
|
128
|
-
const root = findTypecheckRoot(this.baseDir, existingFiles);
|
|
129
|
-
if (root) {
|
|
130
|
-
try {
|
|
131
|
-
typecheckError = await runTypecheck(root);
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
typecheckError = null;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
33
|
const doneSteps = plan.steps.filter((s) => s.status === "done").length;
|
|
139
|
-
// done + skipped are both deliberate terminal states: `done` claims the
|
|
140
|
-
// work, `skipped` documents a decision that the step is not needed (the
|
|
141
|
-
// deliverable gate's documented escape hatch). Neither is a lie the
|
|
142
|
-
// audit must catch — a plan with a legitimately skipped step must be
|
|
143
|
-
// able to pass (observed: skipped step 1 kept the task rejected forever).
|
|
144
|
-
const terminalSteps = plan.steps.filter((s) => s.status === "done" || s.status === "skipped").length;
|
|
145
34
|
const totalSteps = plan.steps.length;
|
|
146
|
-
const auditedFiles = [...new Set([...createFiles, ...deleteFiles])];
|
|
147
35
|
let massEditWarning = null;
|
|
148
|
-
if (
|
|
36
|
+
if (uniqueFiles.length > MASS_EDIT_THRESHOLD) {
|
|
149
37
|
massEditWarning = t("exec.mass_edit_warning", {
|
|
150
|
-
count: String(
|
|
38
|
+
count: String(uniqueFiles.length),
|
|
151
39
|
});
|
|
152
40
|
}
|
|
153
|
-
const
|
|
154
|
-
const typecheckFailing = typecheckError !== null;
|
|
155
|
-
const passed = missingFiles.length === 0 && leftoverFiles.length === 0 && !testsFailing && !typecheckFailing;
|
|
156
|
-
// A "Task complete ✓" summary for an unfinished plan is a lie the model
|
|
157
|
-
// reads as a green light (observed: audit gate injected "[✓] Task
|
|
158
|
-
// complete: 1/3 steps done" while the plan had 2 pending steps). The
|
|
159
|
-
// summary must only claim completion when every step is actually done.
|
|
160
|
-
const stepsPending = terminalSteps < totalSteps;
|
|
41
|
+
const passed = missingFiles.length === 0;
|
|
161
42
|
let summary;
|
|
162
|
-
if (
|
|
163
|
-
summary = t("exec.
|
|
164
|
-
done: doneSteps,
|
|
165
|
-
total: totalSteps,
|
|
166
|
-
files: missingFiles.length,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
else if (leftoverFiles.length > 0) {
|
|
170
|
-
summary = t("exec.audit_leftovers", {
|
|
171
|
-
done: doneSteps,
|
|
172
|
-
total: totalSteps,
|
|
173
|
-
files: leftoverFiles.length,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
else if (testsFailing) {
|
|
177
|
-
summary = t("exec.audit_fail_tests", {
|
|
178
|
-
done: doneSteps,
|
|
179
|
-
total: totalSteps,
|
|
180
|
-
failed: String(testRun.failed),
|
|
181
|
-
passed: String(testRun.passedCount),
|
|
182
|
-
detail: testRun.detail,
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
else if (typecheckFailing) {
|
|
186
|
-
summary = t("exec.audit_fail_typecheck", {
|
|
187
|
-
done: doneSteps,
|
|
188
|
-
total: totalSteps,
|
|
189
|
-
missing: String(missingFiles.length),
|
|
190
|
-
typeError: typecheckError,
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
else if (stepsPending) {
|
|
194
|
-
summary = t("exec.audit_pending", {
|
|
43
|
+
if (passed) {
|
|
44
|
+
summary = t("exec.audit_pass", {
|
|
195
45
|
done: doneSteps,
|
|
196
46
|
total: totalSteps,
|
|
47
|
+
files: existingFiles.length,
|
|
197
48
|
});
|
|
198
49
|
}
|
|
199
50
|
else {
|
|
200
|
-
summary = t("exec.
|
|
51
|
+
summary = t("exec.audit_fail", {
|
|
201
52
|
done: doneSteps,
|
|
202
53
|
total: totalSteps,
|
|
203
|
-
files:
|
|
54
|
+
files: missingFiles.length,
|
|
204
55
|
});
|
|
205
56
|
}
|
|
206
57
|
return {
|
|
207
58
|
passed,
|
|
208
59
|
missingFiles,
|
|
209
|
-
leftoverFiles,
|
|
210
60
|
createdFiles: existingFiles,
|
|
211
61
|
modifiedFiles: [],
|
|
212
62
|
summary,
|
|
213
63
|
massEditWarning,
|
|
214
|
-
testRun,
|
|
215
|
-
typecheckError,
|
|
216
64
|
};
|
|
217
65
|
}
|
|
218
66
|
}
|