micro-models-agent 0.51.1 → 0.52.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/dist/cli/commands.js +162 -38
- package/dist/cli/completer.js +5 -5
- package/dist/cli/main.js +42 -54
- package/dist/cli/repl-commands.js +138 -38
- package/dist/cli/repl.js +175 -89
- package/dist/cli/run-result.js +11 -0
- package/dist/cli/security-commands.js +6 -6
- package/dist/cli/setup.js +21 -15
- package/dist/config/config.js +54 -27
- package/dist/config/defaults.js +17 -0
- package/dist/config/domains.js +179 -0
- package/dist/config/index.js +2 -1
- package/dist/config/security.js +28 -8
- package/dist/core/agent.js +162 -30
- package/dist/core/bootstrap.js +94 -17
- package/dist/core/crash-handler.js +51 -0
- package/dist/core/environment.js +199 -0
- package/dist/core/session-logger.js +60 -6
- package/dist/core/version.js +2 -0
- package/dist/i18n/en.json +120 -39
- package/dist/i18n/ru.json +91 -10
- package/dist/llm/openai-compat.js +191 -53
- package/dist/llm/orchestrator.js +5 -3
- package/dist/logger/app-logger.js +50 -4
- package/dist/main.js +1288 -635
- package/dist/modules/browser/session.js +4 -0
- package/dist/modules/certification/cli.js +58 -19
- package/dist/modules/certification/loader.js +2 -1
- package/dist/modules/certification/manifest.js +22 -14
- package/dist/modules/certification/runner.js +91 -5
- package/dist/modules/certification/scenarios.js +290 -7
- package/dist/modules/context/fact-extractor.js +6 -0
- package/dist/modules/context/manager.js +19 -2
- package/dist/modules/execution/audit-runners.js +61 -7
- package/dist/modules/execution/execution-plugin.js +219 -60
- package/dist/modules/execution/module.js +207 -18
- package/dist/modules/execution/moe-executor.js +33 -20
- package/dist/modules/execution/plan-store.js +39 -0
- package/dist/modules/execution/plan-tool.js +188 -19
- package/dist/modules/execution/planner.js +27 -23
- package/dist/modules/execution/stuck-detector.js +244 -8
- package/dist/modules/execution/tracker.js +8 -6
- package/dist/modules/execution/verifier.js +15 -2
- package/dist/modules/hallucination/detector.js +4 -0
- package/dist/modules/hallucination/factual.js +45 -5
- package/dist/modules/indexer/module.js +1 -0
- package/dist/modules/lsp/client.js +123 -12
- package/dist/modules/lsp/index.js +1 -1
- package/dist/modules/lsp/module.js +30 -2
- package/dist/modules/lsp/probe.js +11 -1
- package/dist/modules/lsp/startup-check.js +5 -2
- package/dist/modules/plugins/builtin/lint-on-write.js +144 -41
- package/dist/modules/plugins/manager.js +57 -13
- package/dist/modules/pricing/index.js +61 -0
- package/dist/modules/pricing/prices.js +129 -0
- package/dist/modules/providers/create.js +22 -0
- package/dist/modules/providers/fallback.js +79 -0
- package/dist/modules/providers/health.js +46 -0
- package/dist/modules/providers/index.js +5 -0
- package/dist/modules/providers/manager.js +161 -0
- package/dist/modules/providers/presets.js +128 -0
- package/dist/modules/providers/registry.js +22 -0
- package/dist/modules/providers/types.js +1 -0
- package/dist/modules/registry.js +1 -0
- package/dist/modules/security/command-validator.js +14 -0
- package/dist/modules/security/encryption.js +6 -6
- package/dist/modules/security/network-validator.js +17 -0
- package/dist/modules/security/path-validator.js +22 -26
- package/dist/modules/session/store.js +10 -10
- package/dist/tools/approve.js +1 -0
- package/dist/tools/attach-image.js +12 -0
- package/dist/tools/bash.js +27 -4
- package/dist/tools/browser.js +1 -0
- package/dist/tools/chunk-query.js +1 -0
- package/dist/tools/create-dir.js +1 -0
- package/dist/tools/delete-file.js +1 -0
- package/dist/tools/download-file.js +1 -0
- package/dist/tools/edit-file.js +2 -1
- package/dist/tools/enable-tools.js +1 -0
- package/dist/tools/executor.js +17 -7
- package/dist/tools/file-info.js +1 -0
- package/dist/tools/glob-tool.js +1 -0
- package/dist/tools/grep-tool.js +54 -13
- package/dist/tools/list-dir.js +1 -0
- package/dist/tools/load-skill.js +1 -0
- package/dist/tools/mcp-call.js +1 -0
- package/dist/tools/move-file.js +1 -0
- package/dist/tools/path-utils.js +51 -1
- package/dist/tools/pipeline-run.js +1 -0
- package/dist/tools/process-kill.js +11 -0
- package/dist/tools/process-list.js +1 -0
- package/dist/tools/process-log.js +9 -0
- package/dist/tools/question.js +1 -0
- package/dist/tools/read-file.js +94 -6
- package/dist/tools/recall.js +1 -0
- package/dist/tools/remember.js +1 -0
- package/dist/tools/scope-check.js +7 -5
- package/dist/tools/search-history.js +1 -0
- package/dist/tools/subagent.js +4 -4
- package/dist/tools/web-browse.js +1 -0
- package/dist/tools/web-fetch.js +27 -6
- package/dist/tools/web-search.js +70 -43
- package/dist/tools/write-file.js +1 -0
- package/dist/ui/line-editor.js +142 -23
- package/dist/ui/line-math.js +8 -4
- package/dist/ui/renderer.js +57 -7
- package/package.json +50 -48
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { t } from "../../i18n/index";
|
|
2
2
|
import { detectTestResults } from "../../tools/bash";
|
|
3
3
|
import { forbiddenWindowsCommand } from "./windows-commands";
|
|
4
|
+
import { extractFileLikeTokens, stripUrls } from "../hallucination/js-identifiers";
|
|
4
5
|
import { platform } from "os";
|
|
5
6
|
/**
|
|
6
7
|
* Cooldown (in iterations) between stuck-recovery injections. Exported so the
|
|
@@ -10,6 +11,87 @@ import { platform } from "os";
|
|
|
10
11
|
export const STUCK_RECOVERY_COOLDOWN = 5;
|
|
11
12
|
const MAX_PLAN_WARNINGS_BEFORE_BLOCK = 3;
|
|
12
13
|
const FORCE_SKIP_THRESHOLD = 10;
|
|
14
|
+
/** Re-log the SAME stuck-warning key at most every N iterations (a new key
|
|
15
|
+
* always logs immediately). Prevents the identical warning from flooding
|
|
16
|
+
* every iteration (ses_mt4fn58c: "Tool bash failed N times" after ~30
|
|
17
|
+
* successful writes, "No write/exec" on every read). */
|
|
18
|
+
export const STUCK_WARN_REPEAT_EVERY = 5;
|
|
19
|
+
/** How many file-mutating tool calls (write/edit/bash/download) without an
|
|
20
|
+
* active plan trigger the plan-creation nudge. Evidence-based (rule #10):
|
|
21
|
+
* the agent is clearly working on files, so a plan is warranted. */
|
|
22
|
+
const PLAN_NUDGE_THRESHOLD = 2;
|
|
23
|
+
/** Tools whose success can have changed the filesystem — the plan auto-advance
|
|
24
|
+
* re-checks the current step's deliverables after them. Read-only tools are
|
|
25
|
+
* deliberately excluded (see the onAfterTool advance call below). */
|
|
26
|
+
const FS_MUTATING_TOOLS = new Set([
|
|
27
|
+
"write_file",
|
|
28
|
+
"edit_file",
|
|
29
|
+
"delete_file",
|
|
30
|
+
"move_file",
|
|
31
|
+
"create_dir",
|
|
32
|
+
"bash",
|
|
33
|
+
"download_file",
|
|
34
|
+
"subagent",
|
|
35
|
+
"mcp_call",
|
|
36
|
+
"pipeline_run",
|
|
37
|
+
"browser",
|
|
38
|
+
]);
|
|
39
|
+
function normalizeBrokenPath(p) {
|
|
40
|
+
return p.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
41
|
+
}
|
|
42
|
+
/** Parse a tsc / bun build / node --check error line into its file path.
|
|
43
|
+
* Returns null when the line carries no file anchor (e.g. `error: Could not
|
|
44
|
+
* resolve "..."`). */
|
|
45
|
+
export function parseBrokenFile(line) {
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
// tsc: src/index.tsx(17,15): error TS2322: ...
|
|
48
|
+
const tsc = /^(.+?)\s*\(\d+,\d+\)\s*:\s*error TS\d+/.exec(trimmed);
|
|
49
|
+
if (tsc)
|
|
50
|
+
return normalizeBrokenPath(tsc[1]);
|
|
51
|
+
// bun build / node --check: /abs/file.tsx:1:10: error: ...
|
|
52
|
+
const bn = /^(.+?):\d+:\d+\s*:\s*(?:error|SyntaxError)/.exec(trimmed);
|
|
53
|
+
if (bn)
|
|
54
|
+
return normalizeBrokenPath(bn[1]);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/** Extract per-file compile failures from a write/edit result output. Only
|
|
58
|
+
* the marker lines are inspected (`[Project typecheck failed]: <line>` /
|
|
59
|
+
* `[Syntax check failed]: <line>`) so a diff body can never false-positive. */
|
|
60
|
+
export function extractBrokenFiles(output) {
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const line of output.split("\n")) {
|
|
63
|
+
const marker = /\[(?:Project typecheck failed|Syntax check failed)\]:\s*(.+)$/.exec(line);
|
|
64
|
+
if (!marker)
|
|
65
|
+
continue;
|
|
66
|
+
const errLine = marker[1].trim();
|
|
67
|
+
out.push({ file: parseBrokenFile(errLine) ?? "", error: errLine.slice(0, 300) });
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Gate for `plan update status=done`: refuse while the last write/edit still
|
|
73
|
+
* reports a compile error. A failure with no file anchor blocks the whole
|
|
74
|
+
* project; otherwise a step's named file tokens are matched against the
|
|
75
|
+
* broken files (path-suffix match, case-insensitive). A step naming no files
|
|
76
|
+
* cannot be blamed — its done stays vacuous. Returns the first error to show
|
|
77
|
+
* the model, or null to allow the done.
|
|
78
|
+
*/
|
|
79
|
+
export function stepTypecheckGate(failures, stepDescription) {
|
|
80
|
+
if (failures.size === 0)
|
|
81
|
+
return null;
|
|
82
|
+
const projectError = failures.get("");
|
|
83
|
+
if (projectError)
|
|
84
|
+
return projectError;
|
|
85
|
+
const stepTokens = extractFileLikeTokens(stripUrls(stepDescription)).map((p) => normalizeBrokenPath(p).toLowerCase());
|
|
86
|
+
if (stepTokens.length === 0)
|
|
87
|
+
return null;
|
|
88
|
+
for (const [file, err] of failures) {
|
|
89
|
+
const f = normalizeBrokenPath(file).toLowerCase();
|
|
90
|
+
if (stepTokens.some((tok) => f.endsWith(tok) || tok.endsWith(f)))
|
|
91
|
+
return err;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
13
95
|
/**
|
|
14
96
|
* The execution plugin hooks, extracted verbatim from ExecutionModule.getPlugin().
|
|
15
97
|
* All shared mutable state (pendingMessages, forbiddenBashFailures, state,
|
|
@@ -37,6 +119,8 @@ export function createExecutionPlugin(deps) {
|
|
|
37
119
|
deps.state.consecutivePlanWarnings = 0;
|
|
38
120
|
deps.state.lastStepId = -1;
|
|
39
121
|
deps.state.stuckNotified = false;
|
|
122
|
+
deps.state.mutationsWithoutPlan = 0;
|
|
123
|
+
deps.state.planNudgeSent = false;
|
|
40
124
|
}
|
|
41
125
|
else {
|
|
42
126
|
const step = deps.trackerRef.current?.getCurrentStep();
|
|
@@ -48,38 +132,58 @@ export function createExecutionPlugin(deps) {
|
|
|
48
132
|
}
|
|
49
133
|
deps.stuckDetector.setCurrentStep(step.id, step.description);
|
|
50
134
|
deps.stuckDetector.recordIteration(step.id);
|
|
135
|
+
// A plan is active — any mutation counter no longer applies.
|
|
136
|
+
deps.state.mutationsWithoutPlan = 0;
|
|
137
|
+
deps.state.planNudgeSent = false;
|
|
51
138
|
}
|
|
52
139
|
else {
|
|
53
140
|
deps.stuckDetector.reset();
|
|
54
141
|
deps.state.consecutivePlanWarnings = 0;
|
|
55
142
|
deps.state.lastStepId = -1;
|
|
56
143
|
deps.state.stuckNotified = false;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
144
|
+
// Evidence-based plan nudge (rule #10 — no bare iteration counter,
|
|
145
|
+
// no keyword task classification): only after the agent actually
|
|
146
|
+
// made file-mutating tool calls (write/edit/bash/download) without
|
|
147
|
+
// a plan does the agent need a plan.
|
|
148
|
+
const mutations = deps.state.mutationsWithoutPlan;
|
|
149
|
+
if (mutations >= PLAN_NUDGE_THRESHOLD &&
|
|
150
|
+
!deps.state.planNudgeSent &&
|
|
151
|
+
ctx.contextManager) {
|
|
152
|
+
deps.state.planNudgeSent = true;
|
|
65
153
|
ctx.contextManager.addMessage({
|
|
66
154
|
role: "user",
|
|
67
|
-
content: `<system-summary
|
|
155
|
+
content: `<system-summary>${t("exec.plan_nudge", {
|
|
156
|
+
count: String(mutations),
|
|
157
|
+
})}</system-summary>`,
|
|
68
158
|
});
|
|
69
159
|
}
|
|
70
160
|
}
|
|
71
161
|
}
|
|
72
162
|
const stuckReason = deps.stuckDetector.getStuckReason();
|
|
73
163
|
if (stuckReason) {
|
|
74
|
-
// Log once per
|
|
75
|
-
|
|
164
|
+
// Log once per warning episode instead of spamming every iteration:
|
|
165
|
+
// a new warning key logs immediately; the SAME key re-logs only every
|
|
166
|
+
// STUCK_WARN_REPEAT_EVERY iterations. The step-stuck reason keeps its
|
|
167
|
+
// dedicated once-per-episode flag.
|
|
168
|
+
const isStuck = deps.stuckDetector.isStuck();
|
|
169
|
+
const warnKey = deps.stuckDetector.getWarningKey();
|
|
170
|
+
const iter = typeof ctx.iteration === "number" ? ctx.iteration : 0;
|
|
171
|
+
const logIt = isStuck
|
|
172
|
+
? !deps.state.stuckNotified
|
|
173
|
+
: warnKey !== deps.state.lastStuckWarnKey ||
|
|
174
|
+
iter - deps.state.lastStuckWarnIter >= STUCK_WARN_REPEAT_EVERY;
|
|
76
175
|
if (logIt) {
|
|
77
176
|
ctx.logger?.warn(stuckReason);
|
|
78
177
|
ctx.sessionLog?.plan("stuck-warning", stuckReason, typeof ctx.iteration === "number" ? ctx.iteration : undefined);
|
|
79
|
-
|
|
178
|
+
deps.state.lastStuckWarnKey = warnKey;
|
|
179
|
+
deps.state.lastStuckWarnIter = iter;
|
|
180
|
+
if (isStuck)
|
|
80
181
|
deps.state.stuckNotified = true;
|
|
81
182
|
}
|
|
82
183
|
}
|
|
184
|
+
else {
|
|
185
|
+
deps.state.lastStuckWarnKey = "";
|
|
186
|
+
}
|
|
83
187
|
if (deps.stuckDetector.isStuck() ||
|
|
84
188
|
deps.stuckDetector.hasRepetitiveToolCalls() ||
|
|
85
189
|
deps.stuckDetector.hasReadOnlyLoop()) {
|
|
@@ -87,33 +191,28 @@ export function createExecutionPlugin(deps) {
|
|
|
87
191
|
if (currentIter - deps.state.lastRecoveryIteration >= STUCK_RECOVERY_COOLDOWN) {
|
|
88
192
|
const recovery = deps.stuckDetector.getRecoveryMessage();
|
|
89
193
|
if (recovery && ctx.contextManager) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const hints = deps.stuckDetector.getHints();
|
|
110
|
-
if (hints.length > 0 && ctx.contextManager) {
|
|
111
|
-
const hintMsg = t("exec.hints", {
|
|
112
|
-
hints: hints.map((h) => `- ${h}`).join("\n"),
|
|
113
|
-
});
|
|
194
|
+
// Include tsc error context so the model knows WHY it is stuck.
|
|
195
|
+
let tscContext = "";
|
|
196
|
+
if (deps.state.typecheckFailures.size > 0) {
|
|
197
|
+
const currentStep = deps.trackerRef.current?.getCurrentStep();
|
|
198
|
+
if (currentStep) {
|
|
199
|
+
const stepFiles = extractFileLikeTokens(stripUrls(currentStep.description));
|
|
200
|
+
for (const [file, error] of deps.state.typecheckFailures) {
|
|
201
|
+
if (!file || stepFiles.some((f) => file.endsWith(f) || f.endsWith(file))) {
|
|
202
|
+
tscContext = `\nLast compile error: ${error}`;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (!tscContext) {
|
|
207
|
+
const first = deps.state.typecheckFailures.entries().next().value;
|
|
208
|
+
if (first)
|
|
209
|
+
tscContext = `\nLast compile error: ${first[1]}`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
114
213
|
ctx.contextManager.addMessage({
|
|
115
214
|
role: "user",
|
|
116
|
-
content: `<system-summary>${
|
|
215
|
+
content: `<system-summary>${recovery}${tscContext}</system-summary>`,
|
|
117
216
|
});
|
|
118
217
|
}
|
|
119
218
|
deps.stuckDetector.recordEscalation();
|
|
@@ -131,7 +230,11 @@ export function createExecutionPlugin(deps) {
|
|
|
131
230
|
const step = deps.trackerRef.current?.getCurrentStep();
|
|
132
231
|
ctx.contextManager.addMessage({
|
|
133
232
|
role: "user",
|
|
134
|
-
content: `<system-summary
|
|
233
|
+
content: `<system-summary>${t("exec.stop_directive", {
|
|
234
|
+
stepId: String(step?.id ?? "?"),
|
|
235
|
+
description: step?.description ?? "",
|
|
236
|
+
iterations: String(deps.stuckDetector.getIterationsOnCurrentStep()),
|
|
237
|
+
})}</system-summary>`,
|
|
135
238
|
});
|
|
136
239
|
}
|
|
137
240
|
}
|
|
@@ -168,6 +271,15 @@ export function createExecutionPlugin(deps) {
|
|
|
168
271
|
const args = ctx?.args;
|
|
169
272
|
if (toolName && args) {
|
|
170
273
|
deps.stuckDetector.recordToolCall(toolName, args);
|
|
274
|
+
// Count file-mutating calls made with NO active plan — feeds the
|
|
275
|
+
// evidence-based plan-creation nudge in onBeforeThink.
|
|
276
|
+
if (!deps.trackerRef.current &&
|
|
277
|
+
(toolName === "write_file" ||
|
|
278
|
+
toolName === "edit_file" ||
|
|
279
|
+
toolName === "bash" ||
|
|
280
|
+
toolName === "download_file")) {
|
|
281
|
+
deps.state.mutationsWithoutPlan++;
|
|
282
|
+
}
|
|
171
283
|
}
|
|
172
284
|
},
|
|
173
285
|
onAfterTool: (ctx, call, result) => {
|
|
@@ -176,15 +288,59 @@ export function createExecutionPlugin(deps) {
|
|
|
176
288
|
if (call.name === "bash") {
|
|
177
289
|
deps.stuckDetector.recordBashOutput(String(call.arguments?.command ?? ""), String(result.output ?? ""));
|
|
178
290
|
}
|
|
291
|
+
// Auto-detect delete intent from model actions: when delete_file is
|
|
292
|
+
// called, find the matching plan step and set kind="delete". This is
|
|
293
|
+
// language-agnostic — based on what the model DID, not what it said.
|
|
294
|
+
if (call.name === "delete_file" && result.success && deps.trackerRef.current) {
|
|
295
|
+
const deletedPath = String(call.arguments?.path ?? "");
|
|
296
|
+
if (deletedPath) {
|
|
297
|
+
const plan = deps.trackerRef.current.getPlan();
|
|
298
|
+
const tokens = extractFileLikeTokens(stripUrls(deletedPath));
|
|
299
|
+
for (const step of plan.steps) {
|
|
300
|
+
if (step.status !== "pending" && step.status !== "in_progress")
|
|
301
|
+
continue;
|
|
302
|
+
const stepTokens = extractFileLikeTokens(stripUrls(step.description));
|
|
303
|
+
const overlap = tokens.some((t) => stepTokens.some((st) => t.endsWith(st) || st.endsWith(t)));
|
|
304
|
+
if (overlap && step.kind !== "delete") {
|
|
305
|
+
step.kind = "delete";
|
|
306
|
+
deps.store.saveActive(plan);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
179
312
|
// A write/edit/bash that reports success but still carries a
|
|
180
313
|
// type/syntax error in its output is NOT a success — the model must
|
|
181
314
|
// fix the actual error, not keep rewriting the file blindly. Feed it
|
|
182
315
|
// to the stuck detector so the rewrite-loop hint fires.
|
|
183
316
|
const toolText = String(result.output ?? "");
|
|
184
|
-
|
|
317
|
+
const hasTypeError = /error TS\d+|\[Project typecheck failed\]|\[Syntax check failed\]/.test(toolText);
|
|
318
|
+
if (hasTypeError) {
|
|
185
319
|
deps.stuckDetector.recordToolError(call.name, toolText.slice(0, 300));
|
|
186
320
|
}
|
|
321
|
+
// Track compile failures per-file so the plan done-gate can refuse
|
|
322
|
+
// "done" while the last edit still doesn't compile (observed: the agent
|
|
323
|
+
// marked plan steps done while the project typecheck failed on every
|
|
324
|
+
// write — a "fix it later" cascade). A CLEAN result clears the map: the
|
|
325
|
+
// debounced project tsc re-ran and passed (or no tsconfig applies), so
|
|
326
|
+
// old failures must not block completion forever.
|
|
327
|
+
if (call.name === "write_file" || call.name === "edit_file") {
|
|
328
|
+
const broken = extractBrokenFiles(toolText);
|
|
329
|
+
if (broken.length > 0) {
|
|
330
|
+
for (const b of broken)
|
|
331
|
+
deps.state.typecheckFailures.set(b.file, b.error);
|
|
332
|
+
}
|
|
333
|
+
else if (result.success) {
|
|
334
|
+
deps.state.typecheckFailures.clear();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
187
337
|
if (!result.success) {
|
|
338
|
+
// Feed the bash attempt outcome into the flailing window BEFORE the
|
|
339
|
+
// per-command handling — rotation across different failing commands
|
|
340
|
+
// (wc → cat → Get-Content → …) must still trip the detector.
|
|
341
|
+
if (call.name === "bash") {
|
|
342
|
+
deps.stuckDetector.recordBashAttempt(false);
|
|
343
|
+
}
|
|
188
344
|
// Hard stop for forbidden Windows commands: after the second
|
|
189
345
|
// failure of the same forbidden command (grep/sed/ls/find/…),
|
|
190
346
|
// inject a STOP message so the session stops burning bash calls
|
|
@@ -205,25 +361,20 @@ export function createExecutionPlugin(deps) {
|
|
|
205
361
|
}
|
|
206
362
|
}
|
|
207
363
|
}
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const parts = [...actionableHints];
|
|
216
|
-
if (alternative) {
|
|
217
|
-
parts.push(`Tool "${call.name}" crashed. Try "${alternative}" instead.`);
|
|
218
|
-
}
|
|
219
|
-
deps.pendingMessages.push({
|
|
220
|
-
role: "user",
|
|
221
|
-
content: `<system-summary>${t("exec.hints", { hints: parts.map((h) => `- ${h}`).join("\n") })}</system-summary>`,
|
|
222
|
-
});
|
|
364
|
+
// A type/syntax error in an otherwise "successful" result was already
|
|
365
|
+
// recorded above. A failing call (result.success === false) would
|
|
366
|
+
// otherwise double-count the same error (typecheck branch + failure
|
|
367
|
+
// branch), inflating the error-signature count and triggering the
|
|
368
|
+
// web search at half the intended threshold.
|
|
369
|
+
if (!hasTypeError) {
|
|
370
|
+
deps.stuckDetector.recordToolError(call.name, result.output);
|
|
223
371
|
}
|
|
224
372
|
}
|
|
225
373
|
else {
|
|
226
374
|
deps.stuckDetector.recordToolSuccess();
|
|
375
|
+
if (call.name === "bash") {
|
|
376
|
+
deps.stuckDetector.recordBashAttempt(true);
|
|
377
|
+
}
|
|
227
378
|
// When a bash command runs code successfully, suggest marking the
|
|
228
379
|
// step done — unless the output shows failing tests, which must
|
|
229
380
|
// never be reported as a clean success.
|
|
@@ -242,12 +393,6 @@ export function createExecutionPlugin(deps) {
|
|
|
242
393
|
content: `<system-summary>${testRun.framework}: all ${testRun.passed} test(s) passed for "${cmd}". You may mark the current step as done via plan update step=N status=done.</system-summary>`,
|
|
243
394
|
});
|
|
244
395
|
}
|
|
245
|
-
else if (/node|tsx|ts-node|python|npm\s+(start|test|run)/.test(cmd)) {
|
|
246
|
-
deps.pendingMessages.push({
|
|
247
|
-
role: "user",
|
|
248
|
-
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>`,
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
396
|
}
|
|
252
397
|
}
|
|
253
398
|
if (result.success && (call.name === "write_file" || call.name === "edit_file")) {
|
|
@@ -265,8 +410,22 @@ export function createExecutionPlugin(deps) {
|
|
|
265
410
|
}
|
|
266
411
|
}
|
|
267
412
|
}
|
|
413
|
+
}
|
|
414
|
+
// Auto-advance fires after every successful FILESYSTEM-MUTATING tool —
|
|
415
|
+
// not just write_file/edit_file. Small models often create files through
|
|
416
|
+
// bash (`echo ... > f`, heredocs) or delete/move via shell instead of the
|
|
417
|
+
// dedicated tools, and the plan then never advances past the step even
|
|
418
|
+
// though its deliverables exist (certification 3.5/3.8: files on disk,
|
|
419
|
+
// plan stuck at 1/4 and 4/5). Read-only tools MUST stay excluded: a
|
|
420
|
+
// successful read/glob would otherwise complete an "Update X" step whose
|
|
421
|
+
// target file already exists from an earlier step — without any edit.
|
|
422
|
+
// advancePlanIfStepComplete is a no-op when tokens don't resolve.
|
|
423
|
+
if (result.success && FS_MUTATING_TOOLS.has(call.name)) {
|
|
268
424
|
deps.advancePlanIfStepComplete(ctx.contextManager, ctx.sessionLog);
|
|
269
425
|
}
|
|
426
|
+
// Automatic web search for a repeatedly failing error (fire-and-forget,
|
|
427
|
+
// results land in pendingMessages on the next onBeforeThink flush).
|
|
428
|
+
deps.maybeSearchError(ctx, call);
|
|
270
429
|
},
|
|
271
430
|
};
|
|
272
431
|
}
|