micro-models-agent 0.39.0 → 0.40.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 +116 -3
- package/dist/cli/main.js +35 -8
- package/dist/cli/repl-commands.js +633 -0
- package/dist/cli/repl.js +110 -611
- package/dist/cli/setup.js +32 -12
- package/dist/config/config.js +46 -30
- package/dist/config/defaults.js +10 -1
- package/dist/config/security.js +15 -8
- package/dist/core/agent-moe.js +24 -12
- package/dist/core/agent.js +281 -47
- package/dist/core/bootstrap.js +52 -36
- package/dist/core/session-logger.js +35 -2
- package/dist/core/workspace.js +76 -0
- package/dist/i18n/en.json +79 -15
- package/dist/i18n/index.js +12 -9
- package/dist/i18n/ru.json +79 -15
- package/dist/index.js +13 -13
- package/dist/llm/openai-compat.js +39 -10
- package/dist/logger/app-logger.js +83 -16
- package/dist/logger/file-log.js +151 -0
- package/dist/main.js +537 -284
- package/dist/modules/browser/bridge-server.mjs +113 -105
- package/dist/modules/browser/session.js +108 -60
- package/dist/modules/certification/cli.js +176 -0
- package/dist/modules/certification/fact-checker.js +84 -0
- package/dist/modules/certification/loader.js +111 -0
- package/dist/modules/certification/manifest.js +50 -0
- package/dist/modules/certification/runner.js +162 -0
- package/dist/modules/certification/scenarios.js +124 -0
- package/dist/modules/certification/types.js +1 -0
- package/dist/modules/context/manager.js +119 -10
- package/dist/modules/execution/auditor.js +33 -39
- package/dist/modules/execution/index.js +8 -6
- package/dist/modules/execution/module.js +474 -32
- package/dist/modules/execution/moe-executor.js +97 -40
- package/dist/modules/execution/plan-coverage.js +68 -0
- package/dist/modules/execution/plan-persister.js +46 -0
- package/dist/modules/execution/plan-store.js +159 -0
- package/dist/modules/execution/planner.js +63 -13
- package/dist/modules/execution/stuck-detector.js +252 -39
- package/dist/modules/execution/tracker.js +21 -7
- package/dist/modules/execution/verifier.js +46 -17
- package/dist/modules/hallucination/confidence.js +7 -2
- package/dist/modules/hallucination/consistency.js +8 -42
- package/dist/modules/hallucination/detector.js +26 -21
- package/dist/modules/hallucination/factual.js +170 -150
- package/dist/modules/hallucination/index.js +5 -4
- package/dist/modules/hallucination/js-identifiers.js +72 -0
- package/dist/modules/hallucination/llm-judge.js +103 -0
- package/dist/modules/index.js +5 -5
- package/dist/modules/lsp/client.js +235 -0
- package/dist/modules/lsp/config.js +81 -0
- package/dist/modules/lsp/index.js +3 -0
- package/dist/modules/lsp/module.js +68 -0
- package/dist/modules/lsp/types.js +1 -0
- package/dist/modules/mcp/client.js +8 -2
- package/dist/modules/memory/store.js +4 -0
- package/dist/modules/plugins/builtin/lint-on-write.js +143 -38
- package/dist/modules/processes/index.js +1 -2
- package/dist/modules/processes/registry.js +125 -35
- package/dist/modules/processes/runner.js +9 -110
- package/dist/modules/security/audit-log.js +30 -10
- package/dist/modules/security/command-validator.js +42 -16
- package/dist/modules/security/content-scanner.js +9 -8
- package/dist/modules/security/network-validator.js +2 -2
- package/dist/modules/security/path-validator.js +64 -10
- package/dist/modules/security/security-policies.js +221 -67
- package/dist/modules/security/session-encryption.js +42 -25
- package/dist/modules/session/manager.js +15 -10
- package/dist/modules/session/store.js +62 -8
- package/dist/modules/skills/index.js +2 -3
- package/dist/modules/skills/module.js +10 -23
- package/dist/tools/bash.js +287 -90
- package/dist/tools/create-dir.js +0 -1
- package/dist/tools/delete-file.js +0 -1
- package/dist/tools/edit-file.js +10 -8
- package/dist/tools/executor.js +57 -7
- package/dist/tools/grep-tool.js +51 -29
- package/dist/tools/index.js +55 -40
- package/dist/tools/load-skill.js +14 -18
- package/dist/tools/move-file.js +3 -2
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/read-file.js +15 -5
- package/dist/tools/search-history.js +42 -22
- package/dist/tools/subagent.js +21 -12
- package/dist/tools/web-browse.js +54 -25
- package/dist/tools/web-fetch.js +60 -34
- package/dist/tools/web-search.js +39 -20
- package/dist/tools/write-file.js +13 -10
- package/dist/ui/diff.js +9 -16
- package/dist/ui/renderer.js +69 -6
- package/package.json +48 -45
- package/dist/modules/context/history.js +0 -15
- package/dist/modules/processes/detect.js +0 -34
- package/dist/modules/skills/matcher.js +0 -27
|
@@ -4,29 +4,68 @@ import { PlanTracker } from "./tracker";
|
|
|
4
4
|
import { StepVerifier } from "./verifier";
|
|
5
5
|
import { StuckDetector } from "./stuck-detector";
|
|
6
6
|
import { Auditor } from "./auditor";
|
|
7
|
-
import {
|
|
7
|
+
import { PlanStore } from "./plan-store";
|
|
8
|
+
import { checkPlanCoverage } from "./plan-coverage";
|
|
9
|
+
import { getMessageText } from "../../llm/provider";
|
|
10
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
11
|
import { resolve } from "path";
|
|
9
12
|
const STUCK_RECOVERY_COOLDOWN = 5;
|
|
13
|
+
const MAX_PLAN_WARNINGS_BEFORE_BLOCK = 3;
|
|
14
|
+
const FORCE_SKIP_THRESHOLD = 10;
|
|
10
15
|
export class ExecutionModule {
|
|
11
16
|
name = "execution";
|
|
12
17
|
tracker = null;
|
|
13
18
|
verifier;
|
|
14
19
|
stuckDetector;
|
|
15
20
|
auditor;
|
|
21
|
+
store;
|
|
16
22
|
baseDir;
|
|
17
23
|
lastRecoveryIteration = -STUCK_RECOVERY_COOLDOWN;
|
|
18
|
-
|
|
24
|
+
consecutivePlanWarnings = 0;
|
|
25
|
+
lastStepId = -1;
|
|
26
|
+
// How many times the deps-step gate has warned for each step id — used to
|
|
27
|
+
// escalate from a soft hint to a MUST-update instruction (models otherwise
|
|
28
|
+
// spin on "install dependencies" steps they invented themselves).
|
|
29
|
+
depsGateHints = new Map();
|
|
30
|
+
// Skip the first audit after restoring a plan from disk — gives the user
|
|
31
|
+
// one chance to change the subject before the audit gate locks in.
|
|
32
|
+
_auditSkipsRemaining = 0;
|
|
33
|
+
stuckNotified = false;
|
|
34
|
+
/**
|
|
35
|
+
* Deferred <system-summary> messages. Injected by onBeforeTool/onAfterTool
|
|
36
|
+
* but flushed in onBeforeThink, so they never land between an assistant
|
|
37
|
+
* tool_call message and its tool results (which breaks message pairing and
|
|
38
|
+
* buries the warnings the model should react to).
|
|
39
|
+
*/
|
|
40
|
+
pendingMessages = [];
|
|
41
|
+
constructor(baseDir, stuckThreshold = 6) {
|
|
19
42
|
this.baseDir = baseDir;
|
|
20
43
|
this.verifier = new StepVerifier(baseDir);
|
|
21
44
|
this.stuckDetector = new StuckDetector(stuckThreshold);
|
|
22
45
|
this.auditor = new Auditor(baseDir);
|
|
46
|
+
this.store = new PlanStore(baseDir);
|
|
23
47
|
}
|
|
24
48
|
setPlan(plan) {
|
|
25
49
|
this.tracker = new PlanTracker(plan);
|
|
50
|
+
this.store.saveActive(plan);
|
|
51
|
+
this._auditSkipsRemaining = 0; // plan explicitly created — arm the audit gate
|
|
52
|
+
}
|
|
53
|
+
restorePlan() {
|
|
54
|
+
const plan = this.store.loadActive();
|
|
55
|
+
if (!plan)
|
|
56
|
+
return false;
|
|
57
|
+
this.tracker = new PlanTracker(plan);
|
|
58
|
+
// Advance to the first non-done step
|
|
59
|
+
this.tracker.syncCurrentStep();
|
|
60
|
+
this._auditSkipsRemaining = 1; // restored plan — one free pass
|
|
61
|
+
return true;
|
|
26
62
|
}
|
|
27
63
|
getTracker() {
|
|
28
64
|
return this.tracker;
|
|
29
65
|
}
|
|
66
|
+
getStore() {
|
|
67
|
+
return this.store;
|
|
68
|
+
}
|
|
30
69
|
getStuckDetector() {
|
|
31
70
|
return this.stuckDetector;
|
|
32
71
|
}
|
|
@@ -37,11 +76,15 @@ export class ExecutionModule {
|
|
|
37
76
|
async runFinalAudit() {
|
|
38
77
|
if (!this.tracker)
|
|
39
78
|
return null;
|
|
79
|
+
if (this._auditSkipsRemaining > 0) {
|
|
80
|
+
this._auditSkipsRemaining--;
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
40
83
|
const plan = this.tracker.getPlan();
|
|
41
84
|
const audit = await this.auditor.audit(plan);
|
|
42
|
-
const pendingSteps = plan.steps
|
|
43
|
-
|
|
44
|
-
|
|
85
|
+
const pendingSteps = plan.steps.flatMap((s) => s.status !== "done" && s.status !== "skipped"
|
|
86
|
+
? [`${s.id}. ${s.description}`]
|
|
87
|
+
: []);
|
|
45
88
|
const done = plan.steps.filter((s) => s.status === "done").length;
|
|
46
89
|
const passed = audit.passed && pendingSteps.length === 0;
|
|
47
90
|
return {
|
|
@@ -67,24 +110,106 @@ export class ExecutionModule {
|
|
|
67
110
|
return [
|
|
68
111
|
{
|
|
69
112
|
name: "plan",
|
|
70
|
-
description: 'Create, update, show, or
|
|
113
|
+
description: 'Create, update, show, abort, list, switch, or re-plan multi-step plans.\n\nActions:\n- create: Start a new plan. Previous active plan is auto-preserved: incomplete → draft, complete → archive.\n- update: Mark step status (done/failed/skipped), or rebuild plan with new steps.\n- show: Print current plan checklist.\n- abort: Archive current plan and clear active slot.\n- list: Show all plans (active, drafts, archived) with progress.\n- switch: Make a different plan active (by plan id).\n- re-plan: Iterative replanning: keep completed steps, replace remaining with new steps.\n\nWrite CONCRETE steps with exact file paths and commands:\n- Specify WHICH files to create with exact paths (e.g. "create src/components/Header.tsx with navigation and logo")\n- Specify WHICH packages to install (e.g. "run npm install react react-dom")\n- Specify WHICH CLI commands to run with exact arguments\n- Each step should include at least one file extension (.ts, .tsx, .json, etc.) or a command verb (install, create, build, run, add, init)\n- Good: "Создать src/components/Header.tsx с навигацией и логотипом"\n- Bad: "Настройка проекта" (too vague — what exactly needs to be configured?)',
|
|
71
114
|
parameters: {
|
|
72
115
|
type: "object",
|
|
73
116
|
properties: {
|
|
74
117
|
action: {
|
|
75
118
|
type: "string",
|
|
76
|
-
enum: [
|
|
119
|
+
enum: [
|
|
120
|
+
"create",
|
|
121
|
+
"update",
|
|
122
|
+
"show",
|
|
123
|
+
"abort",
|
|
124
|
+
"list",
|
|
125
|
+
"switch",
|
|
126
|
+
"re-plan",
|
|
127
|
+
],
|
|
77
128
|
},
|
|
78
129
|
title: { type: "string" },
|
|
79
130
|
steps: { type: "array", items: { type: "string" } },
|
|
80
131
|
step: { type: "number" },
|
|
81
132
|
status: { type: "string", enum: ["done", "failed", "skipped"] },
|
|
82
133
|
note: { type: "string" },
|
|
134
|
+
id: { type: "string", description: "Plan id (for switch action)" },
|
|
83
135
|
},
|
|
84
136
|
required: ["action"],
|
|
85
137
|
},
|
|
86
138
|
handler: async (_ctx, args) => {
|
|
87
139
|
const action = String(args.action);
|
|
140
|
+
if (action === "list") {
|
|
141
|
+
const metas = this.store.listAll();
|
|
142
|
+
if (metas.length === 0) {
|
|
143
|
+
return { success: true, output: t("plan.list_empty") };
|
|
144
|
+
}
|
|
145
|
+
const lines = metas.map((m) => {
|
|
146
|
+
const icon = m.status === "active"
|
|
147
|
+
? "[*]"
|
|
148
|
+
: m.status === "draft"
|
|
149
|
+
? "[ ]"
|
|
150
|
+
: "[-]";
|
|
151
|
+
const namePart = m.name ? ` (${m.name})` : "";
|
|
152
|
+
return `${icon} ${m.id}${namePart} — ${m.title} ${m.doneCount}/${m.stepCount}`;
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
success: true,
|
|
156
|
+
output: `${t("plan.list_header")}\n${lines.join("\n")}`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (action === "switch") {
|
|
160
|
+
const planId = String(args.id || "");
|
|
161
|
+
if (!planId) {
|
|
162
|
+
return { success: false, output: t("plan.switch_no_id") };
|
|
163
|
+
}
|
|
164
|
+
const found = this.store.find(planId);
|
|
165
|
+
if (!found) {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
output: t("plan.not_found", { id: planId }),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
this.preserveActive();
|
|
172
|
+
if (found.status === "draft") {
|
|
173
|
+
this.store.removeDraft(found.plan.id);
|
|
174
|
+
}
|
|
175
|
+
else if (found.status === "archived") {
|
|
176
|
+
this.store.removeArchived(found.plan.id);
|
|
177
|
+
}
|
|
178
|
+
this.setPlan(found.plan);
|
|
179
|
+
const display = PlanCreator.toPromptBlock(found.plan, 0);
|
|
180
|
+
return {
|
|
181
|
+
success: true,
|
|
182
|
+
output: t("plan.switched", {
|
|
183
|
+
id: found.plan.id,
|
|
184
|
+
title: found.plan.title,
|
|
185
|
+
}),
|
|
186
|
+
display,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (action === "re-plan") {
|
|
190
|
+
if (!this.tracker) {
|
|
191
|
+
return { success: false, output: t("plan.no_active") };
|
|
192
|
+
}
|
|
193
|
+
const newSteps = Array.isArray(args.steps)
|
|
194
|
+
? args.steps.map(String)
|
|
195
|
+
: [];
|
|
196
|
+
if (newSteps.length === 0) {
|
|
197
|
+
return { success: false, output: t("plan.replan_no_steps") };
|
|
198
|
+
}
|
|
199
|
+
const oldPlan = this.tracker.getPlan();
|
|
200
|
+
const replanned = PlanCreator.replan(oldPlan, newSteps, args.title ? String(args.title) : undefined);
|
|
201
|
+
const keptCount = replanned.steps.length - newSteps.length;
|
|
202
|
+
this.setPlan(replanned);
|
|
203
|
+
const display = PlanCreator.toPromptBlock(replanned, keptCount);
|
|
204
|
+
return {
|
|
205
|
+
success: true,
|
|
206
|
+
output: t("plan.replanned", {
|
|
207
|
+
kept: String(keptCount),
|
|
208
|
+
steps: String(newSteps.length),
|
|
209
|
+
}),
|
|
210
|
+
display,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
88
213
|
if (action === "create") {
|
|
89
214
|
const title = String(args.title || "Task Plan");
|
|
90
215
|
const steps = Array.isArray(args.steps)
|
|
@@ -93,13 +218,30 @@ export class ExecutionModule {
|
|
|
93
218
|
if (steps.length === 0) {
|
|
94
219
|
return { success: false, output: t("plan.no_steps") };
|
|
95
220
|
}
|
|
96
|
-
|
|
221
|
+
this.preserveActive();
|
|
222
|
+
const plan = PlanCreator.createPlan(title, steps, this.baseDir);
|
|
97
223
|
this.setPlan(plan);
|
|
98
224
|
const display = PlanCreator.toPromptBlock(plan, 0);
|
|
225
|
+
let output = t("plan.created", {
|
|
226
|
+
title,
|
|
227
|
+
steps: String(steps.length),
|
|
228
|
+
});
|
|
229
|
+
let displayOut = display;
|
|
230
|
+
const taskText = this.getTaskText(_ctx);
|
|
231
|
+
if (taskText) {
|
|
232
|
+
const { missing } = checkPlanCoverage(taskText, steps);
|
|
233
|
+
if (missing.length > 0) {
|
|
234
|
+
const warn = t("plan.coverage_warning", {
|
|
235
|
+
missing: missing.join(", "),
|
|
236
|
+
});
|
|
237
|
+
output += `\n⚠️ ${warn}`;
|
|
238
|
+
displayOut = `${display}\n⚠️ ${warn}`;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
99
241
|
return {
|
|
100
242
|
success: true,
|
|
101
|
-
output
|
|
102
|
-
display,
|
|
243
|
+
output,
|
|
244
|
+
display: displayOut,
|
|
103
245
|
};
|
|
104
246
|
}
|
|
105
247
|
if (action === "show") {
|
|
@@ -117,16 +259,42 @@ export class ExecutionModule {
|
|
|
117
259
|
this.tracker.updateStepStatus(Number(args.step), args.status || "done");
|
|
118
260
|
if (args.note)
|
|
119
261
|
this.tracker.addNote(Number(args.step), String(args.note));
|
|
262
|
+
this.tracker.syncCurrentStep();
|
|
263
|
+
this.store.saveActive(this.tracker.getPlan());
|
|
120
264
|
const progress = this.tracker.getProgressString();
|
|
121
265
|
const display = this.tracker.toPromptBlock();
|
|
266
|
+
_ctx.sessionLog?.plan("step-update", `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })} | ${progress} | current: step ${this.tracker.getCurrentStepIndex() + 1}`);
|
|
122
267
|
return {
|
|
123
268
|
success: true,
|
|
124
269
|
output: `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })}\n${progress}`,
|
|
125
270
|
display,
|
|
126
271
|
};
|
|
127
272
|
}
|
|
273
|
+
if (action === "update" &&
|
|
274
|
+
Array.isArray(args.steps) &&
|
|
275
|
+
args.steps.length > 0 &&
|
|
276
|
+
this.tracker) {
|
|
277
|
+
const title = String(args.title || this.tracker.getPlan().title);
|
|
278
|
+
const steps = args.steps.map(String);
|
|
279
|
+
const plan = PlanCreator.createPlan(title, steps, this.baseDir);
|
|
280
|
+
this.setPlan(plan);
|
|
281
|
+
const display = PlanCreator.toPromptBlock(plan, 0);
|
|
282
|
+
const output = t("plan.updated", {
|
|
283
|
+
title,
|
|
284
|
+
steps: String(steps.length),
|
|
285
|
+
});
|
|
286
|
+
return {
|
|
287
|
+
success: true,
|
|
288
|
+
output,
|
|
289
|
+
display,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
128
292
|
if (action === "abort") {
|
|
293
|
+
if (this.tracker) {
|
|
294
|
+
this.store.archivePlan(this.tracker.getPlan());
|
|
295
|
+
}
|
|
129
296
|
this.tracker = null;
|
|
297
|
+
this.store.clearActive();
|
|
130
298
|
return { success: true, output: t("plan.aborted") };
|
|
131
299
|
}
|
|
132
300
|
if (!this.tracker) {
|
|
@@ -172,9 +340,20 @@ export class ExecutionModule {
|
|
|
172
340
|
};
|
|
173
341
|
}
|
|
174
342
|
if (action === "done") {
|
|
343
|
+
if (!currentStep) {
|
|
344
|
+
return { success: false, output: t("plan.step_not_found") };
|
|
345
|
+
}
|
|
346
|
+
this.tracker.updateStepStatus(currentStep.id, "done");
|
|
347
|
+
this.tracker.syncCurrentStep();
|
|
348
|
+
this.store.saveActive(this.tracker.getPlan());
|
|
349
|
+
const display = this.tracker.toPromptBlock();
|
|
175
350
|
return {
|
|
176
351
|
success: true,
|
|
177
|
-
output: t("
|
|
352
|
+
output: t("plan.step_status", {
|
|
353
|
+
step: String(currentStep.id),
|
|
354
|
+
status: "done",
|
|
355
|
+
}),
|
|
356
|
+
display,
|
|
178
357
|
};
|
|
179
358
|
}
|
|
180
359
|
if (action === "list") {
|
|
@@ -224,17 +403,64 @@ export class ExecutionModule {
|
|
|
224
403
|
return {
|
|
225
404
|
name: "execution",
|
|
226
405
|
onBeforeThink: (ctx) => {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
this.
|
|
406
|
+
// Flush deferred <system-summary> messages first so they are visible
|
|
407
|
+
// to the next model call but never interleave tool messages.
|
|
408
|
+
if (ctx.contextManager && this.pendingMessages.length > 0) {
|
|
409
|
+
for (const m of this.pendingMessages.splice(0)) {
|
|
410
|
+
ctx.contextManager.addMessage(m);
|
|
411
|
+
}
|
|
231
412
|
}
|
|
232
|
-
|
|
413
|
+
if (this.tracker?.isComplete()) {
|
|
414
|
+
// All steps done/skipped — stop counting "no progress" against the
|
|
415
|
+
// last step and silence stuck warnings for a finished plan.
|
|
233
416
|
this.stuckDetector.reset();
|
|
417
|
+
this.consecutivePlanWarnings = 0;
|
|
418
|
+
this.lastStepId = -1;
|
|
419
|
+
this.stuckNotified = false;
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
const step = this.tracker?.getCurrentStep();
|
|
423
|
+
if (this.tracker && step) {
|
|
424
|
+
if (step.id !== this.lastStepId) {
|
|
425
|
+
this.consecutivePlanWarnings = 0;
|
|
426
|
+
this.lastStepId = step.id;
|
|
427
|
+
this.stuckNotified = false;
|
|
428
|
+
}
|
|
429
|
+
this.stuckDetector.setCurrentStep(step.id, step.description);
|
|
430
|
+
this.stuckDetector.recordIteration(step.id);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
this.stuckDetector.reset();
|
|
434
|
+
this.consecutivePlanWarnings = 0;
|
|
435
|
+
this.lastStepId = -1;
|
|
436
|
+
this.stuckNotified = false;
|
|
437
|
+
const iter = typeof ctx.iteration === "number" ? ctx.iteration : 0;
|
|
438
|
+
if (iter === 3 && !this.tracker && ctx.contextManager) {
|
|
439
|
+
ctx.contextManager.addMessage({
|
|
440
|
+
role: "user",
|
|
441
|
+
content: `<system-summary>You have made 3 tool calls without creating a plan. For any task that involves creating files, installing packages, or multiple steps — you MUST use plan create BEFORE continuing. Use the plan tool now with concrete steps (exact filenames, commands, deliverables). Do NOT make any more write/edit/bash calls until you have a plan.</system-summary>`,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
if (iter >= 6 && !this.tracker && ctx.contextManager) {
|
|
445
|
+
ctx.contextManager.addMessage({
|
|
446
|
+
role: "user",
|
|
447
|
+
content: `<system-summary>STOP. ${iter} iterations without a plan. You MUST call plan create RIGHT NOW. No more tool calls until you create a plan.</system-summary>`,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
234
451
|
}
|
|
235
452
|
const stuckReason = this.stuckDetector.getStuckReason();
|
|
236
453
|
if (stuckReason) {
|
|
237
|
-
|
|
454
|
+
// Log once per stuck episode instead of spamming every iteration.
|
|
455
|
+
const logIt = this.stuckDetector.isStuck()
|
|
456
|
+
? !this.stuckNotified
|
|
457
|
+
: true;
|
|
458
|
+
if (logIt) {
|
|
459
|
+
ctx.logger?.warn(stuckReason);
|
|
460
|
+
ctx.sessionLog?.plan("stuck-warning", stuckReason, typeof ctx.iteration === "number" ? ctx.iteration : undefined);
|
|
461
|
+
if (this.stuckDetector.isStuck())
|
|
462
|
+
this.stuckNotified = true;
|
|
463
|
+
}
|
|
238
464
|
}
|
|
239
465
|
if (this.stuckDetector.isStuck() ||
|
|
240
466
|
this.stuckDetector.hasRepetitiveToolCalls()) {
|
|
@@ -243,22 +469,80 @@ export class ExecutionModule {
|
|
|
243
469
|
STUCK_RECOVERY_COOLDOWN) {
|
|
244
470
|
const recovery = this.stuckDetector.getRecoveryMessage();
|
|
245
471
|
if (recovery && ctx.contextManager) {
|
|
472
|
+
const lastError = this.stuckDetector.getLastErrorOutput();
|
|
473
|
+
const skillHint = lastError
|
|
474
|
+
? "\nIf you have relevant skills available, consider loading one with load_skill for expert guidance."
|
|
475
|
+
: "";
|
|
476
|
+
// Actionable hints based on actual error output
|
|
477
|
+
const actionableHints = this.stuckDetector.getActionableHints();
|
|
478
|
+
const actionableHintStr = actionableHints.length > 0
|
|
479
|
+
? `\n${t("exec.hints", { hints: actionableHints.map((h) => `- ${h}`).join("\n") })}`
|
|
480
|
+
: "";
|
|
481
|
+
// Tool alternative suggestion
|
|
482
|
+
const alternative = this.stuckDetector.getToolAlternative();
|
|
483
|
+
const altHint = alternative
|
|
484
|
+
? `\nTool "${this.stuckDetector.getLastFailedTool()}" is failing. Try "${alternative}" instead.`
|
|
485
|
+
: "";
|
|
246
486
|
ctx.contextManager.addMessage({
|
|
247
487
|
role: "user",
|
|
248
|
-
content: `<system-summary>${recovery}</system-summary>`,
|
|
488
|
+
content: `<system-summary>${recovery}${skillHint}${actionableHintStr}${altHint}</system-summary>`,
|
|
249
489
|
});
|
|
250
490
|
}
|
|
491
|
+
const hints = this.stuckDetector.getHints();
|
|
492
|
+
if (hints.length > 0 && ctx.contextManager) {
|
|
493
|
+
const hintMsg = t("exec.hints", {
|
|
494
|
+
hints: hints.map((h) => `- ${h}`).join("\n"),
|
|
495
|
+
});
|
|
496
|
+
ctx.contextManager.addMessage({
|
|
497
|
+
role: "user",
|
|
498
|
+
content: `<system-summary>${hintMsg}</system-summary>`,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
this.stuckDetector.recordEscalation();
|
|
251
502
|
this.lastRecoveryIteration = currentIter;
|
|
503
|
+
if (this.stuckDetector.shouldEscalate() && ctx.onMeta) {
|
|
504
|
+
const escalation = t("exec.escalation", {
|
|
505
|
+
stepId: String(this.tracker?.getCurrentStep()?.id ?? "?"),
|
|
506
|
+
description: this.tracker?.getCurrentStep()?.description ?? "",
|
|
507
|
+
});
|
|
508
|
+
ctx.onMeta(escalation);
|
|
509
|
+
}
|
|
510
|
+
// Force skip after too many iterations on the same step
|
|
511
|
+
if (this.stuckDetector.getIterationsOnCurrentStep() >=
|
|
512
|
+
FORCE_SKIP_THRESHOLD &&
|
|
513
|
+
ctx.contextManager) {
|
|
514
|
+
const step = this.tracker?.getCurrentStep();
|
|
515
|
+
ctx.contextManager.addMessage({
|
|
516
|
+
role: "user",
|
|
517
|
+
content: `<system-summary>STOP. Step ${step?.id ?? "?"} ("${step?.description ?? ""}") took ${this.stuckDetector.getIterationsOnCurrentStep()} iterations with no progress. DO NOT continue this step. Immediately call: plan update step=${step?.id ?? "?"} status=done (if code works despite warnings) OR plan update step=${step?.id ?? "?"} status=skipped note="reason". Do NOT make any other tool calls before updating the plan.</system-summary>`,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
252
520
|
}
|
|
253
521
|
}
|
|
254
522
|
},
|
|
255
|
-
onBeforeTool: (
|
|
523
|
+
onBeforeTool: (_ctx, call) => {
|
|
256
524
|
const warning = this.checkPlanAlignment(call);
|
|
257
|
-
if (warning
|
|
258
|
-
|
|
525
|
+
if (warning) {
|
|
526
|
+
this.pendingMessages.push({
|
|
259
527
|
role: "user",
|
|
260
528
|
content: `<system-summary>${warning}</system-summary>`,
|
|
261
529
|
});
|
|
530
|
+
this.consecutivePlanWarnings++;
|
|
531
|
+
if (this.consecutivePlanWarnings >= MAX_PLAN_WARNINGS_BEFORE_BLOCK) {
|
|
532
|
+
this.pendingMessages.push({
|
|
533
|
+
role: "user",
|
|
534
|
+
content: `<system-summary>${t("exec.plan_blocked", { step: String(this.tracker?.getCurrentStep()?.id ?? "?"), max: MAX_PLAN_WARNINGS_BEFORE_BLOCK })}</system-summary>`,
|
|
535
|
+
});
|
|
536
|
+
// Block with an explicit reason so the model sees WHY the call
|
|
537
|
+
// was blocked (executor shows it via tool.blocked_reason).
|
|
538
|
+
return t("exec.plan_blocked", {
|
|
539
|
+
step: String(this.tracker?.getCurrentStep()?.id ?? "?"),
|
|
540
|
+
max: MAX_PLAN_WARNINGS_BEFORE_BLOCK,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
this.consecutivePlanWarnings = 0;
|
|
262
546
|
}
|
|
263
547
|
return true;
|
|
264
548
|
},
|
|
@@ -271,19 +555,68 @@ export class ExecutionModule {
|
|
|
271
555
|
},
|
|
272
556
|
onAfterTool: (ctx, call, result) => {
|
|
273
557
|
if (!result.success) {
|
|
274
|
-
this.stuckDetector.recordToolError(call.name);
|
|
558
|
+
this.stuckDetector.recordToolError(call.name, result.output);
|
|
559
|
+
// Immediately queue an actionable hint on failure (don't wait for
|
|
560
|
+
// the stuck threshold). Flushed in onBeforeThink so it does not
|
|
561
|
+
// land between tool messages.
|
|
562
|
+
const actionableHints = this.stuckDetector.getActionableHints();
|
|
563
|
+
const alternative = this.stuckDetector.getToolAlternative();
|
|
564
|
+
if (actionableHints.length > 0 || alternative) {
|
|
565
|
+
const parts = [...actionableHints];
|
|
566
|
+
if (alternative) {
|
|
567
|
+
parts.push(`Tool "${call.name}" crashed. Try "${alternative}" instead.`);
|
|
568
|
+
}
|
|
569
|
+
this.pendingMessages.push({
|
|
570
|
+
role: "user",
|
|
571
|
+
content: `<system-summary>${t("exec.hints", { hints: parts.map((h) => `- ${h}`).join("\n") })}</system-summary>`,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
275
574
|
}
|
|
276
575
|
else {
|
|
277
576
|
this.stuckDetector.recordToolSuccess();
|
|
577
|
+
// When a bash command runs code successfully, suggest marking step as done
|
|
578
|
+
if (call.name === "bash" && result.success) {
|
|
579
|
+
const cmd = String(call.arguments?.command ?? "");
|
|
580
|
+
if (/node|tsx|ts-node|python|npm\s+(start|test|run)/.test(cmd)) {
|
|
581
|
+
this.pendingMessages.push({
|
|
582
|
+
role: "user",
|
|
583
|
+
content: `<system-summary>The command "${cmd}" completed successfully. If this was testing your code, mark the current step as done via plan update step=N status=done.</system-summary>`,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
278
587
|
}
|
|
279
|
-
if (
|
|
280
|
-
result.success &&
|
|
588
|
+
if (result.success &&
|
|
281
589
|
(call.name === "write_file" || call.name === "edit_file")) {
|
|
282
|
-
|
|
590
|
+
const filePath = call.arguments?.path;
|
|
591
|
+
if (filePath) {
|
|
592
|
+
this.stuckDetector.recordFileRewrite(filePath);
|
|
593
|
+
if (this.stuckDetector.hasExcessiveRewrites()) {
|
|
594
|
+
const file = this.stuckDetector.getExcessiveRewriteFile();
|
|
595
|
+
const count = this.stuckDetector.getFileRewriteCount(file);
|
|
596
|
+
if (ctx.onMeta) {
|
|
597
|
+
ctx.onMeta(t("exec.file_rewrite_warning", {
|
|
598
|
+
file: file,
|
|
599
|
+
count: String(count),
|
|
600
|
+
}));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
this.advancePlanIfStepComplete(ctx.contextManager, ctx.sessionLog);
|
|
283
605
|
}
|
|
284
606
|
},
|
|
285
607
|
};
|
|
286
608
|
}
|
|
609
|
+
preserveActive() {
|
|
610
|
+
if (!this.tracker)
|
|
611
|
+
return;
|
|
612
|
+
const plan = this.tracker.getPlan();
|
|
613
|
+
if (plan.steps.every((s) => s.status === "done" || s.status === "skipped")) {
|
|
614
|
+
this.store.archivePlan(plan);
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
this.store.saveDraft(plan);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
287
620
|
checkPlanAlignment(call) {
|
|
288
621
|
if (!this.tracker)
|
|
289
622
|
return null;
|
|
@@ -300,8 +633,6 @@ export class ExecutionModule {
|
|
|
300
633
|
"grep",
|
|
301
634
|
"file_info",
|
|
302
635
|
"load_skill",
|
|
303
|
-
"question",
|
|
304
|
-
"approve",
|
|
305
636
|
];
|
|
306
637
|
if (allowedAlways.includes(call.name))
|
|
307
638
|
return null;
|
|
@@ -312,7 +643,26 @@ export class ExecutionModule {
|
|
|
312
643
|
const callPaths = (argStr.match(/\b[\w./\\-]+\.[a-z]+/gi) || []).map((p) => p.toLowerCase());
|
|
313
644
|
if (callPaths.length === 0)
|
|
314
645
|
return null;
|
|
315
|
-
|
|
646
|
+
// Files that already belong to COMPLETED (done/skipped) steps are
|
|
647
|
+
// legitimate rewrite targets — the agent iterates on them while fixing
|
|
648
|
+
// LSP/type errors. Blocking those would stop normal bug-fixing.
|
|
649
|
+
const plan = this.tracker.getPlan();
|
|
650
|
+
const finishedPaths = new Set();
|
|
651
|
+
for (const s of plan.steps) {
|
|
652
|
+
if (s.status === "done" || s.status === "skipped") {
|
|
653
|
+
const ps = s.description
|
|
654
|
+
.match(/\b[\w./\\-]+\.[a-z]+/gi)
|
|
655
|
+
?.map((p) => p.toLowerCase()) ?? [];
|
|
656
|
+
ps.forEach((p) => finishedPaths.add(p));
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const offPath = callPaths.some((p) => {
|
|
660
|
+
if (finishedPaths.has(p) ||
|
|
661
|
+
[...finishedPaths].some((s) => s.includes(p))) {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
return !stepPaths.some((s) => p.includes(s) || s.includes(p));
|
|
665
|
+
});
|
|
316
666
|
if (!offPath)
|
|
317
667
|
return null;
|
|
318
668
|
return t("exec.plan_warning", {
|
|
@@ -321,17 +671,109 @@ export class ExecutionModule {
|
|
|
321
671
|
tool: call.name,
|
|
322
672
|
});
|
|
323
673
|
}
|
|
324
|
-
advancePlanIfStepComplete() {
|
|
674
|
+
advancePlanIfStepComplete(contextManager, sessionLog) {
|
|
325
675
|
const step = this.tracker?.getCurrentStep();
|
|
326
676
|
if (!step)
|
|
327
677
|
return;
|
|
678
|
+
const stepText = step.description.toLowerCase();
|
|
328
679
|
const stepPaths = step.description.match(/\b[\w./\\-]+\.[a-z]+/gi) || [];
|
|
680
|
+
// Step gate: verify dependencies when mentioned — check BEFORE file paths
|
|
681
|
+
const isDepsStep = stepText.includes("install") ||
|
|
682
|
+
stepText.includes("зависим") ||
|
|
683
|
+
stepText.includes("init") ||
|
|
684
|
+
stepText.includes("инициализац");
|
|
685
|
+
if (isDepsStep) {
|
|
686
|
+
const lockFiles = [
|
|
687
|
+
"package-lock.json",
|
|
688
|
+
"bun.lockb",
|
|
689
|
+
"yarn.lock",
|
|
690
|
+
"pnpm-lock.yaml",
|
|
691
|
+
"node_modules",
|
|
692
|
+
"go.sum",
|
|
693
|
+
"Cargo.lock",
|
|
694
|
+
"Pipfile.lock",
|
|
695
|
+
"poetry.lock",
|
|
696
|
+
"requirements.txt",
|
|
697
|
+
];
|
|
698
|
+
const hasLockFile = lockFiles.some((f) => existsSync(resolve(this.baseDir, f)));
|
|
699
|
+
if (!hasLockFile) {
|
|
700
|
+
if (contextManager) {
|
|
701
|
+
const hints = this.depsGateHints.get(step.id) || 0;
|
|
702
|
+
this.depsGateHints.set(step.id, hints + 1);
|
|
703
|
+
const force = hints >= 1;
|
|
704
|
+
const content = force
|
|
705
|
+
? t("exec.step_gate_deps_force", { step: String(step.id) })
|
|
706
|
+
: t("exec.step_gate_deps", {
|
|
707
|
+
step: String(step.id),
|
|
708
|
+
description: step.description,
|
|
709
|
+
});
|
|
710
|
+
contextManager.addMessage({
|
|
711
|
+
role: "user",
|
|
712
|
+
content: `<system-summary>${content}</system-summary>`,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
329
718
|
if (stepPaths.length === 0)
|
|
330
719
|
return;
|
|
331
720
|
const allExist = stepPaths.every((p) => existsSync(resolve(this.baseDir, p)));
|
|
332
|
-
if (allExist)
|
|
333
|
-
|
|
334
|
-
|
|
721
|
+
if (!allExist)
|
|
722
|
+
return;
|
|
723
|
+
// Step gate: verify files have content (not empty)
|
|
724
|
+
const emptyFiles = [];
|
|
725
|
+
for (const p of stepPaths) {
|
|
726
|
+
try {
|
|
727
|
+
const content = readFileSync(resolve(this.baseDir, p), "utf-8");
|
|
728
|
+
if (content.trim().length < 10) {
|
|
729
|
+
emptyFiles.push(p);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
// If we can't read it, rely on existsSync result
|
|
734
|
+
}
|
|
335
735
|
}
|
|
736
|
+
if (emptyFiles.length > 0 && contextManager) {
|
|
737
|
+
contextManager.addMessage({
|
|
738
|
+
role: "user",
|
|
739
|
+
content: `<system-summary>${t("exec.step_gate_empty", { step: String(step.id), files: emptyFiles.join(", ") })}</system-summary>`,
|
|
740
|
+
});
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
// All checks passed — advance
|
|
744
|
+
this.tracker?.updateStepStatus(step.id, "done");
|
|
745
|
+
const hadNext = this.tracker?.advance() ?? false;
|
|
746
|
+
if (this.tracker) {
|
|
747
|
+
this.store.saveActive(this.tracker.getPlan());
|
|
748
|
+
sessionLog?.plan("auto-advance", `Step ${step.id} auto-completed | ${this.tracker.getProgressString()} | current: step ${this.tracker.getCurrentStepIndex() + 1}`);
|
|
749
|
+
}
|
|
750
|
+
if (contextManager) {
|
|
751
|
+
const nextStep = hadNext ? this.tracker?.getCurrentStep() : null;
|
|
752
|
+
const nextMsg = nextStep
|
|
753
|
+
? t("exec.step_gate_ok", {
|
|
754
|
+
step: String(step.id),
|
|
755
|
+
nextStep: String(nextStep.id),
|
|
756
|
+
nextDesc: nextStep.description,
|
|
757
|
+
})
|
|
758
|
+
: t("exec.step_gate_last", { step: String(step.id) });
|
|
759
|
+
contextManager.addMessage({
|
|
760
|
+
role: "user",
|
|
761
|
+
content: `<system-summary>${nextMsg}</system-summary>`,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Extract the original task statement from the first user message in the
|
|
767
|
+
* conversation history. Used to validate plan coverage against requirements.
|
|
768
|
+
*/
|
|
769
|
+
getTaskText(ctx) {
|
|
770
|
+
const manager = ctx?.contextManager;
|
|
771
|
+
if (!manager)
|
|
772
|
+
return "";
|
|
773
|
+
const history = manager.getActiveHistory();
|
|
774
|
+
const firstUser = history.find((m) => m.role === "user");
|
|
775
|
+
if (!firstUser)
|
|
776
|
+
return "";
|
|
777
|
+
return getMessageText(firstUser.content).trim();
|
|
336
778
|
}
|
|
337
779
|
}
|