micro-models-agent 0.28.17 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands.js +3 -116
- package/dist/cli/main.js +8 -35
- package/dist/cli/repl.js +611 -110
- package/dist/cli/setup.js +12 -32
- package/dist/config/config.js +30 -46
- package/dist/config/defaults.js +1 -10
- package/dist/config/security.js +8 -15
- package/dist/core/agent-moe.js +12 -24
- package/dist/core/agent.js +47 -281
- package/dist/core/bootstrap.js +36 -52
- package/dist/core/session-logger.js +2 -35
- package/dist/i18n/en.json +15 -79
- package/dist/i18n/index.js +9 -12
- package/dist/i18n/ru.json +15 -79
- package/dist/index.js +13 -13
- package/dist/llm/openai-compat.js +10 -39
- package/dist/logger/app-logger.js +16 -83
- package/dist/main.js +625 -243
- package/dist/modules/browser/session.js +60 -108
- package/dist/modules/context/history.js +15 -0
- package/dist/modules/context/manager.js +10 -119
- package/dist/modules/execution/auditor.js +39 -33
- package/dist/modules/execution/index.js +6 -8
- package/dist/modules/execution/module.js +32 -474
- package/dist/modules/execution/moe-executor.js +40 -97
- package/dist/modules/execution/planner.js +13 -63
- package/dist/modules/execution/stuck-detector.js +39 -252
- package/dist/modules/execution/tracker.js +7 -21
- package/dist/modules/execution/verifier.js +17 -46
- package/dist/modules/hallucination/confidence.js +2 -7
- package/dist/modules/hallucination/consistency.js +42 -8
- package/dist/modules/hallucination/detector.js +21 -26
- package/dist/modules/hallucination/factual.js +150 -170
- package/dist/modules/hallucination/index.js +4 -5
- package/dist/modules/index.js +5 -5
- package/dist/modules/mcp/client.js +2 -8
- package/dist/modules/memory/store.js +0 -4
- package/dist/modules/plugins/builtin/lint-on-write.js +38 -143
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +2 -1
- package/dist/modules/processes/registry.js +35 -125
- package/dist/modules/processes/runner.js +110 -9
- package/dist/modules/security/audit-log.js +10 -30
- package/dist/modules/security/command-validator.js +16 -42
- package/dist/modules/security/content-scanner.js +8 -9
- package/dist/modules/security/network-validator.js +2 -2
- package/dist/modules/security/path-validator.js +10 -64
- package/dist/modules/security/security-policies.js +67 -221
- package/dist/modules/security/session-encryption.js +25 -42
- package/dist/modules/session/manager.js +10 -15
- package/dist/modules/session/store.js +8 -62
- package/dist/modules/skills/index.js +3 -2
- package/dist/modules/skills/matcher.js +27 -0
- package/dist/modules/skills/module.js +23 -10
- package/dist/tools/bash.js +90 -287
- package/dist/tools/create-dir.js +1 -0
- package/dist/tools/delete-file.js +1 -0
- package/dist/tools/edit-file.js +8 -10
- package/dist/tools/executor.js +7 -57
- package/dist/tools/grep-tool.js +29 -51
- package/dist/tools/index.js +40 -55
- package/dist/tools/load-skill.js +18 -14
- package/dist/tools/move-file.js +2 -3
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/read-file.js +5 -15
- package/dist/tools/search-history.js +22 -42
- package/dist/tools/subagent.js +12 -21
- package/dist/tools/web-browse.js +25 -54
- package/dist/tools/web-fetch.js +34 -60
- package/dist/tools/web-search.js +20 -39
- package/dist/tools/write-file.js +10 -13
- package/dist/ui/diff.js +16 -9
- package/dist/ui/renderer.js +6 -69
- package/package.json +1 -1
- package/dist/cli/repl-commands.js +0 -633
- package/dist/core/workspace.js +0 -76
- package/dist/logger/file-log.js +0 -151
- package/dist/modules/certification/cli.js +0 -176
- package/dist/modules/certification/fact-checker.js +0 -84
- package/dist/modules/certification/loader.js +0 -111
- package/dist/modules/certification/manifest.js +0 -50
- package/dist/modules/certification/runner.js +0 -162
- package/dist/modules/certification/scenarios.js +0 -124
- package/dist/modules/certification/types.js +0 -1
- package/dist/modules/execution/plan-coverage.js +0 -68
- package/dist/modules/execution/plan-persister.js +0 -46
- package/dist/modules/execution/plan-store.js +0 -159
- package/dist/modules/hallucination/js-identifiers.js +0 -72
- package/dist/modules/hallucination/llm-judge.js +0 -103
- package/dist/modules/lsp/client.js +0 -235
- package/dist/modules/lsp/config.js +0 -81
- package/dist/modules/lsp/index.js +0 -3
- package/dist/modules/lsp/module.js +0 -68
- package/dist/modules/lsp/types.js +0 -1
|
@@ -4,68 +4,29 @@ 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 {
|
|
8
|
-
import { checkPlanCoverage } from "./plan-coverage";
|
|
9
|
-
import { getMessageText } from "../../llm/provider";
|
|
10
|
-
import { existsSync, readFileSync } from "fs";
|
|
7
|
+
import { existsSync } from "fs";
|
|
11
8
|
import { resolve } from "path";
|
|
12
9
|
const STUCK_RECOVERY_COOLDOWN = 5;
|
|
13
|
-
const MAX_PLAN_WARNINGS_BEFORE_BLOCK = 3;
|
|
14
|
-
const FORCE_SKIP_THRESHOLD = 10;
|
|
15
10
|
export class ExecutionModule {
|
|
16
11
|
name = "execution";
|
|
17
12
|
tracker = null;
|
|
18
13
|
verifier;
|
|
19
14
|
stuckDetector;
|
|
20
15
|
auditor;
|
|
21
|
-
store;
|
|
22
16
|
baseDir;
|
|
23
17
|
lastRecoveryIteration = -STUCK_RECOVERY_COOLDOWN;
|
|
24
|
-
|
|
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) {
|
|
18
|
+
constructor(baseDir, stuckThreshold = 8) {
|
|
42
19
|
this.baseDir = baseDir;
|
|
43
20
|
this.verifier = new StepVerifier(baseDir);
|
|
44
21
|
this.stuckDetector = new StuckDetector(stuckThreshold);
|
|
45
22
|
this.auditor = new Auditor(baseDir);
|
|
46
|
-
this.store = new PlanStore(baseDir);
|
|
47
23
|
}
|
|
48
24
|
setPlan(plan) {
|
|
49
25
|
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;
|
|
62
26
|
}
|
|
63
27
|
getTracker() {
|
|
64
28
|
return this.tracker;
|
|
65
29
|
}
|
|
66
|
-
getStore() {
|
|
67
|
-
return this.store;
|
|
68
|
-
}
|
|
69
30
|
getStuckDetector() {
|
|
70
31
|
return this.stuckDetector;
|
|
71
32
|
}
|
|
@@ -76,15 +37,11 @@ export class ExecutionModule {
|
|
|
76
37
|
async runFinalAudit() {
|
|
77
38
|
if (!this.tracker)
|
|
78
39
|
return null;
|
|
79
|
-
if (this._auditSkipsRemaining > 0) {
|
|
80
|
-
this._auditSkipsRemaining--;
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
40
|
const plan = this.tracker.getPlan();
|
|
84
41
|
const audit = await this.auditor.audit(plan);
|
|
85
|
-
const pendingSteps = plan.steps
|
|
86
|
-
|
|
87
|
-
|
|
42
|
+
const pendingSteps = plan.steps
|
|
43
|
+
.filter((s) => s.status !== "done" && s.status !== "skipped")
|
|
44
|
+
.map((s) => `${s.id}. ${s.description}`);
|
|
88
45
|
const done = plan.steps.filter((s) => s.status === "done").length;
|
|
89
46
|
const passed = audit.passed && pendingSteps.length === 0;
|
|
90
47
|
return {
|
|
@@ -110,106 +67,24 @@ export class ExecutionModule {
|
|
|
110
67
|
return [
|
|
111
68
|
{
|
|
112
69
|
name: "plan",
|
|
113
|
-
description: 'Create, update, show, abort
|
|
70
|
+
description: 'Create, update, show, or abort a multi-step plan. Use "create" at the start of complex tasks. Use "update" after completing each step to track progress. Use "show" to re-print the current plan checklist.',
|
|
114
71
|
parameters: {
|
|
115
72
|
type: "object",
|
|
116
73
|
properties: {
|
|
117
74
|
action: {
|
|
118
75
|
type: "string",
|
|
119
|
-
enum: [
|
|
120
|
-
"create",
|
|
121
|
-
"update",
|
|
122
|
-
"show",
|
|
123
|
-
"abort",
|
|
124
|
-
"list",
|
|
125
|
-
"switch",
|
|
126
|
-
"re-plan",
|
|
127
|
-
],
|
|
76
|
+
enum: ["create", "update", "show", "abort"],
|
|
128
77
|
},
|
|
129
78
|
title: { type: "string" },
|
|
130
79
|
steps: { type: "array", items: { type: "string" } },
|
|
131
80
|
step: { type: "number" },
|
|
132
81
|
status: { type: "string", enum: ["done", "failed", "skipped"] },
|
|
133
82
|
note: { type: "string" },
|
|
134
|
-
id: { type: "string", description: "Plan id (for switch action)" },
|
|
135
83
|
},
|
|
136
84
|
required: ["action"],
|
|
137
85
|
},
|
|
138
86
|
handler: async (_ctx, args) => {
|
|
139
87
|
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
|
-
}
|
|
213
88
|
if (action === "create") {
|
|
214
89
|
const title = String(args.title || "Task Plan");
|
|
215
90
|
const steps = Array.isArray(args.steps)
|
|
@@ -218,30 +93,13 @@ export class ExecutionModule {
|
|
|
218
93
|
if (steps.length === 0) {
|
|
219
94
|
return { success: false, output: t("plan.no_steps") };
|
|
220
95
|
}
|
|
221
|
-
|
|
222
|
-
const plan = PlanCreator.createPlan(title, steps, this.baseDir);
|
|
96
|
+
const plan = PlanCreator.createPlan(title, steps);
|
|
223
97
|
this.setPlan(plan);
|
|
224
98
|
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
|
-
}
|
|
241
99
|
return {
|
|
242
100
|
success: true,
|
|
243
|
-
output,
|
|
244
|
-
display
|
|
101
|
+
output: t("plan.created", { title, steps: String(steps.length) }),
|
|
102
|
+
display,
|
|
245
103
|
};
|
|
246
104
|
}
|
|
247
105
|
if (action === "show") {
|
|
@@ -259,42 +117,16 @@ export class ExecutionModule {
|
|
|
259
117
|
this.tracker.updateStepStatus(Number(args.step), args.status || "done");
|
|
260
118
|
if (args.note)
|
|
261
119
|
this.tracker.addNote(Number(args.step), String(args.note));
|
|
262
|
-
this.tracker.syncCurrentStep();
|
|
263
|
-
this.store.saveActive(this.tracker.getPlan());
|
|
264
120
|
const progress = this.tracker.getProgressString();
|
|
265
121
|
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}`);
|
|
267
122
|
return {
|
|
268
123
|
success: true,
|
|
269
124
|
output: `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })}\n${progress}`,
|
|
270
125
|
display,
|
|
271
126
|
};
|
|
272
127
|
}
|
|
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
|
-
}
|
|
292
128
|
if (action === "abort") {
|
|
293
|
-
if (this.tracker) {
|
|
294
|
-
this.store.archivePlan(this.tracker.getPlan());
|
|
295
|
-
}
|
|
296
129
|
this.tracker = null;
|
|
297
|
-
this.store.clearActive();
|
|
298
130
|
return { success: true, output: t("plan.aborted") };
|
|
299
131
|
}
|
|
300
132
|
if (!this.tracker) {
|
|
@@ -340,20 +172,9 @@ export class ExecutionModule {
|
|
|
340
172
|
};
|
|
341
173
|
}
|
|
342
174
|
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();
|
|
350
175
|
return {
|
|
351
176
|
success: true,
|
|
352
|
-
output: t("
|
|
353
|
-
step: String(currentStep.id),
|
|
354
|
-
status: "done",
|
|
355
|
-
}),
|
|
356
|
-
display,
|
|
177
|
+
output: t("todo.marked_done", { count: "1" }),
|
|
357
178
|
};
|
|
358
179
|
}
|
|
359
180
|
if (action === "list") {
|
|
@@ -403,64 +224,17 @@ export class ExecutionModule {
|
|
|
403
224
|
return {
|
|
404
225
|
name: "execution",
|
|
405
226
|
onBeforeThink: (ctx) => {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
ctx.contextManager.addMessage(m);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
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.
|
|
416
|
-
this.stuckDetector.reset();
|
|
417
|
-
this.consecutivePlanWarnings = 0;
|
|
418
|
-
this.lastStepId = -1;
|
|
419
|
-
this.stuckNotified = false;
|
|
227
|
+
const step = this.tracker?.getCurrentStep();
|
|
228
|
+
if (this.tracker && step) {
|
|
229
|
+
this.stuckDetector.setCurrentStep(step.id, step.description);
|
|
230
|
+
this.stuckDetector.recordIteration(step.id);
|
|
420
231
|
}
|
|
421
232
|
else {
|
|
422
|
-
|
|
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
|
-
}
|
|
233
|
+
this.stuckDetector.reset();
|
|
451
234
|
}
|
|
452
235
|
const stuckReason = this.stuckDetector.getStuckReason();
|
|
453
236
|
if (stuckReason) {
|
|
454
|
-
|
|
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
|
-
}
|
|
237
|
+
ctx.logger?.warn(stuckReason);
|
|
464
238
|
}
|
|
465
239
|
if (this.stuckDetector.isStuck() ||
|
|
466
240
|
this.stuckDetector.hasRepetitiveToolCalls()) {
|
|
@@ -469,80 +243,22 @@ export class ExecutionModule {
|
|
|
469
243
|
STUCK_RECOVERY_COOLDOWN) {
|
|
470
244
|
const recovery = this.stuckDetector.getRecoveryMessage();
|
|
471
245
|
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
|
-
: "";
|
|
486
246
|
ctx.contextManager.addMessage({
|
|
487
247
|
role: "user",
|
|
488
|
-
content: `<system-summary>${recovery}
|
|
248
|
+
content: `<system-summary>${recovery}</system-summary>`,
|
|
489
249
|
});
|
|
490
250
|
}
|
|
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();
|
|
502
251
|
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
|
-
}
|
|
520
252
|
}
|
|
521
253
|
}
|
|
522
254
|
},
|
|
523
|
-
onBeforeTool: (
|
|
255
|
+
onBeforeTool: (ctx, call) => {
|
|
524
256
|
const warning = this.checkPlanAlignment(call);
|
|
525
|
-
if (warning) {
|
|
526
|
-
|
|
257
|
+
if (warning && ctx.contextManager) {
|
|
258
|
+
ctx.contextManager.addMessage({
|
|
527
259
|
role: "user",
|
|
528
260
|
content: `<system-summary>${warning}</system-summary>`,
|
|
529
261
|
});
|
|
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;
|
|
546
262
|
}
|
|
547
263
|
return true;
|
|
548
264
|
},
|
|
@@ -555,68 +271,19 @@ export class ExecutionModule {
|
|
|
555
271
|
},
|
|
556
272
|
onAfterTool: (ctx, call, result) => {
|
|
557
273
|
if (!result.success) {
|
|
558
|
-
this.stuckDetector.recordToolError(call.name
|
|
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
|
-
}
|
|
274
|
+
this.stuckDetector.recordToolError(call.name);
|
|
574
275
|
}
|
|
575
276
|
else {
|
|
576
277
|
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
|
-
}
|
|
587
278
|
}
|
|
588
|
-
if (
|
|
279
|
+
if (this.tracker &&
|
|
280
|
+
result.success &&
|
|
589
281
|
(call.name === "write_file" || call.name === "edit_file")) {
|
|
590
|
-
|
|
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);
|
|
282
|
+
this.advancePlanIfStepComplete();
|
|
605
283
|
}
|
|
606
284
|
},
|
|
607
285
|
};
|
|
608
286
|
}
|
|
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
|
-
}
|
|
620
287
|
checkPlanAlignment(call) {
|
|
621
288
|
if (!this.tracker)
|
|
622
289
|
return null;
|
|
@@ -633,6 +300,8 @@ export class ExecutionModule {
|
|
|
633
300
|
"grep",
|
|
634
301
|
"file_info",
|
|
635
302
|
"load_skill",
|
|
303
|
+
"question",
|
|
304
|
+
"approve",
|
|
636
305
|
];
|
|
637
306
|
if (allowedAlways.includes(call.name))
|
|
638
307
|
return null;
|
|
@@ -643,26 +312,7 @@ export class ExecutionModule {
|
|
|
643
312
|
const callPaths = (argStr.match(/\b[\w./\\-]+\.[a-z]+/gi) || []).map((p) => p.toLowerCase());
|
|
644
313
|
if (callPaths.length === 0)
|
|
645
314
|
return null;
|
|
646
|
-
|
|
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
|
-
});
|
|
315
|
+
const offPath = callPaths.some((p) => !stepPaths.some((s) => p.includes(s) || s.includes(p)));
|
|
666
316
|
if (!offPath)
|
|
667
317
|
return null;
|
|
668
318
|
return t("exec.plan_warning", {
|
|
@@ -671,109 +321,17 @@ export class ExecutionModule {
|
|
|
671
321
|
tool: call.name,
|
|
672
322
|
});
|
|
673
323
|
}
|
|
674
|
-
advancePlanIfStepComplete(
|
|
324
|
+
advancePlanIfStepComplete() {
|
|
675
325
|
const step = this.tracker?.getCurrentStep();
|
|
676
326
|
if (!step)
|
|
677
327
|
return;
|
|
678
|
-
const stepText = step.description.toLowerCase();
|
|
679
328
|
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
|
-
}
|
|
718
329
|
if (stepPaths.length === 0)
|
|
719
330
|
return;
|
|
720
331
|
const allExist = stepPaths.every((p) => existsSync(resolve(this.baseDir, p)));
|
|
721
|
-
if (
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
}
|
|
332
|
+
if (allExist) {
|
|
333
|
+
this.tracker?.updateStepStatus(step.id, "done");
|
|
334
|
+
this.tracker?.advance();
|
|
735
335
|
}
|
|
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();
|
|
778
336
|
}
|
|
779
337
|
}
|