micro-models-agent 0.63.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +148 -1
- package/dist/cli/cache-line.js +30 -0
- package/dist/cli/command-suggest.js +38 -0
- package/dist/cli/commands.js +285 -60
- package/dist/cli/completer.js +16 -16
- package/dist/cli/json-payload.js +32 -0
- package/dist/cli/main.js +165 -77
- package/dist/cli/plugin-commands.js +5 -4
- package/dist/cli/relaunch.js +37 -0
- package/dist/cli/repl-commands.js +441 -307
- package/dist/cli/repl.js +360 -83
- package/dist/cli/run-result.js +12 -6
- package/dist/cli/security-commands.js +64 -60
- package/dist/cli/setup-order.js +57 -0
- package/dist/cli/setup-prompt.js +49 -0
- package/dist/cli/setup.js +52 -48
- package/dist/config/budget.js +48 -0
- package/dist/config/config.js +132 -70
- package/dist/config/defaults.js +37 -11
- package/dist/config/domains.js +9 -50
- package/dist/config/utils.js +56 -0
- package/dist/core/agent/audit-gate.js +49 -0
- package/dist/core/agent/compaction.js +89 -0
- package/dist/core/agent/constants.js +61 -0
- package/dist/core/agent/context-renderer.js +40 -0
- package/dist/core/agent/hallucination-gate.js +87 -0
- package/dist/core/agent/loop-state.js +53 -0
- package/dist/core/agent/prefix-monitor.js +101 -0
- package/dist/core/agent/reasoning-resolver.js +56 -0
- package/dist/core/agent/token-tracker.js +96 -0
- package/dist/core/agent/tool-batch.js +237 -0
- package/dist/core/agent/tool-output.js +62 -0
- package/dist/core/agent-moe.js +214 -69
- package/dist/core/agent.js +506 -546
- package/dist/core/bootstrap.js +297 -98
- package/dist/core/crash-handler.js +2 -1
- package/dist/core/prompt-builder.js +3 -0
- package/dist/core/prompt-overflow.js +307 -0
- package/dist/core/session-logger.js +34 -2
- package/dist/i18n/en.json +7 -4
- package/dist/i18n/ru.json +7 -4
- package/dist/index.js +5 -1
- package/dist/llm/cache-usage.js +76 -0
- package/dist/llm/image-utils.js +20 -16
- package/dist/llm/llm-errors.js +41 -0
- package/dist/llm/model-loader.js +30 -0
- package/dist/llm/openai-compat.js +287 -101
- package/dist/llm/orchestrator.js +140 -68
- package/dist/llm/provider-budget.js +68 -0
- package/dist/llm/provider.js +0 -1
- package/dist/llm/stream-state.js +26 -0
- package/dist/llm/token-counter.js +28 -0
- package/dist/logger/app-logger.js +12 -15
- package/dist/main.js +1606 -800
- package/dist/migration/detect.js +3 -1
- package/dist/modules/browser/actions.js +0 -3
- package/dist/modules/browser/bridge-client.js +2 -0
- package/dist/modules/browser/driver.js +46 -4
- package/dist/modules/certification/cli.js +85 -42
- package/dist/modules/certification/loader.js +15 -1
- package/dist/modules/certification/manifest.js +126 -15
- package/dist/modules/certification/runner.js +4 -26
- package/dist/modules/certification/scenarios.js +184 -5
- package/dist/modules/certification/syntax-scenarios.js +51 -0
- package/dist/modules/context/chunk-query.js +25 -5
- package/dist/modules/context/fact-extractor.js +6 -2
- package/dist/modules/context/manager.js +23 -7
- package/dist/modules/execution/audit-runners.js +7 -1
- package/dist/modules/execution/auditor.js +3 -3
- package/dist/modules/execution/execution-plugin.js +22 -15
- package/dist/modules/execution/input-from.js +46 -0
- package/dist/modules/execution/module.js +107 -18
- package/dist/modules/execution/moe-executor.js +166 -54
- package/dist/modules/execution/plan-actions.js +524 -0
- package/dist/modules/execution/plan-steps.js +23 -0
- package/dist/modules/execution/plan-store.js +15 -3
- package/dist/modules/execution/plan-tool.js +6 -488
- package/dist/modules/execution/plan-validator.js +24 -0
- package/dist/modules/execution/stuck-detector.js +3 -18
- package/dist/modules/execution/tracker.js +14 -5
- package/dist/modules/execution/transient-error.js +30 -0
- package/dist/modules/execution/verifier.js +94 -7
- package/dist/modules/execution/windows-commands.js +11 -0
- package/dist/modules/hallucination/confidence.js +36 -23
- package/dist/modules/hallucination/consistency.js +3 -0
- package/dist/modules/hallucination/detector.js +8 -3
- package/dist/modules/hallucination/factual.js +26 -7
- package/dist/modules/hallucination/llm-judge.js +12 -2
- package/dist/modules/indexer/map-command.js +35 -0
- package/dist/modules/indexer/map-select.js +87 -0
- package/dist/modules/indexer/module.js +34 -22
- package/dist/modules/indexer/symbols.js +189 -0
- package/dist/modules/indexer/walker.js +96 -42
- package/dist/modules/lsp/check-tool.js +2 -1
- package/dist/modules/lsp/client.js +49 -32
- package/dist/modules/lsp/config.js +55 -2
- package/dist/modules/lsp/module.js +38 -5
- package/dist/modules/lsp/probe.js +4 -3
- package/dist/modules/lsp/project-root.js +41 -1
- package/dist/modules/lsp/startup-check.js +12 -4
- package/dist/modules/mcp/client.js +153 -104
- package/dist/modules/mcp/module.js +165 -41
- package/dist/modules/memory/module.js +4 -3
- package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
- package/dist/modules/plugins/manager.js +47 -84
- package/dist/modules/pricing/index.js +17 -7
- package/dist/modules/pricing/prices.js +30 -12
- package/dist/modules/processes/index.js +1 -0
- package/dist/modules/processes/kill-tree.js +56 -0
- package/dist/modules/processes/registry.js +2 -54
- package/dist/modules/providers/cache.js +23 -0
- package/dist/modules/providers/factory.js +28 -0
- package/dist/modules/providers/fallback.js +7 -5
- package/dist/modules/providers/health.js +2 -1
- package/dist/modules/providers/index.js +1 -0
- package/dist/modules/providers/manager.js +17 -2
- package/dist/modules/providers/presets.js +79 -6
- package/dist/modules/reasoning/policy.js +40 -0
- package/dist/modules/reasoning/probe.js +111 -0
- package/dist/modules/security/audit-notifier.js +42 -27
- package/dist/modules/security/command-validator.js +25 -20
- package/dist/modules/security/encryption.js +6 -12
- package/dist/modules/security/network-validator.js +76 -5
- package/dist/modules/security/path-validator.js +77 -34
- package/dist/modules/security/rate-limiter.js +11 -0
- package/dist/modules/security/security-policies.js +1 -1
- package/dist/modules/security/session-encryption.js +13 -2
- package/dist/modules/security/session-isolation.js +2 -9
- package/dist/modules/session/manager.js +11 -0
- package/dist/modules/session/module.js +11 -3
- package/dist/modules/session/store.js +41 -5
- package/dist/modules/skills/loader.js +7 -1
- package/dist/modules/skills/module.js +2 -1
- package/dist/modules/updater/changelog-reader.js +94 -0
- package/dist/modules/updater/dev-detect.js +17 -0
- package/dist/modules/updater/index.js +1 -0
- package/dist/modules/updater/module.js +14 -3
- package/dist/output/bus.js +32 -0
- package/dist/output/channel.js +233 -0
- package/dist/output/format.js +14 -0
- package/dist/output/index.js +7 -0
- package/dist/output/json-sink.js +22 -0
- package/dist/output/machine.js +8 -0
- package/dist/output/session-sink.js +27 -0
- package/dist/output/types.js +1 -0
- package/dist/tools/approve.js +6 -2
- package/dist/tools/attach-image.js +11 -11
- package/dist/tools/auto-fixer.js +198 -0
- package/dist/tools/bash.js +142 -89
- package/dist/tools/chunk-query.js +10 -6
- package/dist/tools/download-file.js +1 -1
- package/dist/tools/edit-file.js +20 -2
- package/dist/tools/executor.js +54 -9
- package/dist/tools/glob-tool.js +7 -0
- package/dist/tools/grep-tool.js +15 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/list-dir.js +3 -1
- package/dist/tools/load-skill.js +2 -1
- package/dist/tools/mcp-call.js +1 -1
- package/dist/tools/move-file.js +5 -4
- package/dist/tools/path-utils.js +7 -0
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/prompt-io.js +28 -0
- package/dist/tools/question.js +12 -12
- package/dist/tools/scope-request.js +91 -0
- package/dist/tools/session-info.js +44 -0
- package/dist/tools/set-thinking.js +71 -0
- package/dist/tools/subagent.js +50 -9
- package/dist/tools/syntax-validator.js +177 -0
- package/dist/tools/user-input.js +16 -9
- package/dist/tools/write-file.js +17 -1
- package/dist/ui/diff.js +10 -0
- package/dist/ui/line-editor.js +179 -26
- package/dist/ui/line-math.js +20 -3
- package/dist/ui/md-formatter.js +100 -10
- package/dist/ui/output.js +5 -4
- package/dist/ui/plan-view.js +2 -7
- package/dist/ui/renderer.js +89 -85
- package/dist/ui/spinner.js +14 -4
- package/dist/utils/error.js +4 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/retry.js +17 -0
- package/dist/utils/sleep.js +23 -0
- package/dist/utils/truncate.js +9 -0
- package/package.json +1 -1
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { t } from "../../i18n/index";
|
|
2
|
-
import {
|
|
3
|
-
import { checkPlanCoverage } from "./plan-coverage";
|
|
4
|
-
import { stepTypecheckGate } from "./execution-plugin";
|
|
2
|
+
import { dispatchPlanAction } from "./plan-actions";
|
|
5
3
|
/**
|
|
6
4
|
* The plan / todo / verify tool definitions for the execution module. The
|
|
7
5
|
* handlers were extracted verbatim from ExecutionModule.getToolDefinitions() —
|
|
@@ -46,492 +44,12 @@ export function createPlanToolDefinitions(deps) {
|
|
|
46
44
|
},
|
|
47
45
|
required: ["action"],
|
|
48
46
|
},
|
|
49
|
-
handler: async (
|
|
47
|
+
handler: async (ctx, args) => {
|
|
50
48
|
const action = String(args.action);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
const lines = metas.map((m) => {
|
|
57
|
-
const icon = m.status === "active" ? "[*]" : m.status === "draft" ? "[ ]" : "[-]";
|
|
58
|
-
const namePart = m.name ? ` (${m.name})` : "";
|
|
59
|
-
return `${icon} ${m.id}${namePart} — ${m.title} ${m.doneCount}/${m.stepCount}`;
|
|
60
|
-
});
|
|
61
|
-
lines.push("");
|
|
62
|
-
lines.push(t("plan.list_legend"));
|
|
63
|
-
return {
|
|
64
|
-
success: true,
|
|
65
|
-
output: `${t("plan.list_header")}\n${lines.join("\n")}`,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
if (action === "switch") {
|
|
69
|
-
const planId = String(args.id || "");
|
|
70
|
-
if (!planId) {
|
|
71
|
-
return { success: false, output: t("plan.switch_no_id") };
|
|
72
|
-
}
|
|
73
|
-
const found = deps.store.find(planId);
|
|
74
|
-
if (!found) {
|
|
75
|
-
return {
|
|
76
|
-
success: false,
|
|
77
|
-
output: t("plan.not_found", { id: planId }),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
// Switching to the plan that is already active must be a no-op, not
|
|
81
|
-
// a duplicate (observed: switch on the active plan preserved the
|
|
82
|
-
// active.json and re-set the same plan, leaving two copies).
|
|
83
|
-
if (found.status === "active") {
|
|
84
|
-
return {
|
|
85
|
-
success: true,
|
|
86
|
-
output: t("plan.already_active", {
|
|
87
|
-
id: found.plan.id,
|
|
88
|
-
title: found.plan.title,
|
|
89
|
-
}),
|
|
90
|
-
display: deps.trackerRef.current?.toPromptBlock(),
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
deps.preserveActive();
|
|
94
|
-
if (found.status === "draft") {
|
|
95
|
-
deps.store.removeDraft(found.plan.id);
|
|
96
|
-
}
|
|
97
|
-
else if (found.status === "archived") {
|
|
98
|
-
deps.store.removeArchived(found.plan.id);
|
|
99
|
-
}
|
|
100
|
-
deps.setPlan(found.plan);
|
|
101
|
-
// Display from the tracker (setPlan syncs the pointer past any
|
|
102
|
-
// done/skipped steps) so "current: step N" points at the first
|
|
103
|
-
// pending step of the switched-to plan, not always step 1.
|
|
104
|
-
const display = deps.trackerRef.current?.toPromptBlock();
|
|
105
|
-
return {
|
|
106
|
-
success: true,
|
|
107
|
-
output: t("plan.switched", {
|
|
108
|
-
id: found.plan.id,
|
|
109
|
-
title: found.plan.title,
|
|
110
|
-
}),
|
|
111
|
-
display,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
if (action === "re-plan") {
|
|
115
|
-
if (!deps.trackerRef.current) {
|
|
116
|
-
return { success: false, output: t("plan.no_active") };
|
|
117
|
-
}
|
|
118
|
-
const newSteps = Array.isArray(args.steps) ? args.steps.map(String) : [];
|
|
119
|
-
if (newSteps.length === 0) {
|
|
120
|
-
return { success: false, output: t("plan.replan_no_steps") };
|
|
121
|
-
}
|
|
122
|
-
const tracker = deps.trackerRef.current;
|
|
123
|
-
const oldPlan = tracker.getPlan();
|
|
124
|
-
const replanned = PlanCreator.replan(oldPlan, newSteps, args.title ? String(args.title) : undefined);
|
|
125
|
-
const keptCount = replanned.steps.length - newSteps.length;
|
|
126
|
-
deps.setPlan(replanned);
|
|
127
|
-
const display = deps.trackerRef.current?.toPromptBlock();
|
|
128
|
-
return {
|
|
129
|
-
success: true,
|
|
130
|
-
output: t("plan.replanned", {
|
|
131
|
-
kept: String(keptCount),
|
|
132
|
-
steps: String(newSteps.length),
|
|
133
|
-
}),
|
|
134
|
-
display,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
if (action === "create") {
|
|
138
|
-
const title = String(args.title || "Task Plan");
|
|
139
|
-
const steps = Array.isArray(args.steps) ? args.steps.map(String) : [];
|
|
140
|
-
if (steps.length === 0) {
|
|
141
|
-
return { success: false, output: t("plan.no_steps") };
|
|
142
|
-
}
|
|
143
|
-
const parsed = deps.parseKinds(args, steps);
|
|
144
|
-
if (parsed.error) {
|
|
145
|
-
return { success: false, output: parsed.error };
|
|
146
|
-
}
|
|
147
|
-
// Never silently discard an in-progress plan (observed:
|
|
148
|
-
// ses_msvuao0h — right after compaction the model called
|
|
149
|
-
// `plan create` again, dumping the 1/6 plan into drafts and
|
|
150
|
-
// starting from scratch). An active plan WITH progress blocks
|
|
151
|
-
// creation until the model consciously aborts it. A fresh plan
|
|
152
|
-
// (all steps pending) is safe to replace — the model may have
|
|
153
|
-
// just changed its mind about the approach.
|
|
154
|
-
const existing = deps.trackerRef.current;
|
|
155
|
-
if (existing) {
|
|
156
|
-
const activePlan = existing.getPlan();
|
|
157
|
-
const progressed = activePlan.steps.some((s) => s.status === "done" || s.status === "failed" || s.status === "skipped");
|
|
158
|
-
if (progressed) {
|
|
159
|
-
const done = activePlan.steps.filter((s) => s.status === "done").length;
|
|
160
|
-
return {
|
|
161
|
-
success: false,
|
|
162
|
-
output: t("plan.active_in_progress", {
|
|
163
|
-
id: activePlan.id,
|
|
164
|
-
done: String(done),
|
|
165
|
-
total: String(activePlan.steps.length),
|
|
166
|
-
current: String(existing.getCurrentStepIndex() + 1),
|
|
167
|
-
}),
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
deps.preserveActive();
|
|
172
|
-
const plan = PlanCreator.createPlan(title, steps, deps.baseDir, parsed.kinds ?? []);
|
|
173
|
-
deps.setPlan(plan);
|
|
174
|
-
const display = PlanCreator.toPromptBlock(plan, 0);
|
|
175
|
-
let output = t("plan.created", {
|
|
176
|
-
title,
|
|
177
|
-
steps: String(steps.length),
|
|
178
|
-
});
|
|
179
|
-
// The model sometimes passes an id expecting the plan to be
|
|
180
|
-
// created under it (observed: id "plan_error_check_e07pb2" was
|
|
181
|
-
// ignored, a new id appeared). Be explicit so it doesn't look
|
|
182
|
-
// for a plan by the wrong id afterwards.
|
|
183
|
-
if (args.id) {
|
|
184
|
-
output += `\n${t("plan.id_ignored")}`;
|
|
185
|
-
}
|
|
186
|
-
if (existing) {
|
|
187
|
-
output += `\n${t("plan.existing_fresh")}`;
|
|
188
|
-
}
|
|
189
|
-
let displayOut = display;
|
|
190
|
-
const taskText = deps.getTaskText(_ctx);
|
|
191
|
-
if (taskText) {
|
|
192
|
-
const { missing } = checkPlanCoverage(taskText, steps);
|
|
193
|
-
if (missing.length > 0) {
|
|
194
|
-
const warn = t("plan.coverage_warning", {
|
|
195
|
-
missing: missing.join(", "),
|
|
196
|
-
});
|
|
197
|
-
output += `\n⚠️ ${warn}`;
|
|
198
|
-
displayOut = `${display}\n⚠️ ${warn}`;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return {
|
|
202
|
-
success: true,
|
|
203
|
-
output,
|
|
204
|
-
display: displayOut,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
if (action === "show") {
|
|
208
|
-
const planId = args.id ? String(args.id) : "";
|
|
209
|
-
if (planId && planId !== deps.trackerRef.current?.getPlan()?.id) {
|
|
210
|
-
// Show a specific plan (active, draft, or archived) instead
|
|
211
|
-
// of ignoring the id and always printing the active one
|
|
212
|
-
// (observed: the model asked for an archived plan three
|
|
213
|
-
// times and got the active plan back every time).
|
|
214
|
-
const found = deps.store.find(planId);
|
|
215
|
-
if (!found) {
|
|
216
|
-
return {
|
|
217
|
-
success: false,
|
|
218
|
-
output: t("plan.not_found", { id: planId }),
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
const display = PlanCreator.toPromptBlock(found.plan, 0);
|
|
222
|
-
return {
|
|
223
|
-
success: true,
|
|
224
|
-
output: `${t("plan.show_header")}\n${display}`,
|
|
225
|
-
display,
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
if (!deps.trackerRef.current) {
|
|
229
|
-
return { success: false, output: t("plan.no_active") };
|
|
230
|
-
}
|
|
231
|
-
const display = deps.trackerRef.current.toPromptBlock();
|
|
232
|
-
return {
|
|
233
|
-
success: true,
|
|
234
|
-
output: `${t("plan.show_header")}\n${display}`,
|
|
235
|
-
display,
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
if (action === "update" && args.step && deps.trackerRef.current) {
|
|
239
|
-
const tracker = deps.trackerRef.current;
|
|
240
|
-
const stepId = Number(args.step);
|
|
241
|
-
const target = tracker.getStep(stepId);
|
|
242
|
-
if (!target) {
|
|
243
|
-
return { success: false, output: t("plan.step_not_found") };
|
|
244
|
-
}
|
|
245
|
-
const status = String(args.status || "done");
|
|
246
|
-
// Completed plan: re-marking a done step of a finished plan is the
|
|
247
|
-
// loop anchor (observed: 3× "plan update step=4 done" on a 4/4 plan
|
|
248
|
-
// while a NEW task waited). Archive + clear so the model can create a
|
|
249
|
-
// fresh plan for the new task instead of spinning.
|
|
250
|
-
if (tracker.isComplete()) {
|
|
251
|
-
const plan = tracker.getPlan();
|
|
252
|
-
deps.store.archivePlan(plan);
|
|
253
|
-
deps.recordCompleted(plan); // final audit still verifies it
|
|
254
|
-
deps.trackerRef.current = null;
|
|
255
|
-
const p = getPlanProgress(plan);
|
|
256
|
-
return {
|
|
257
|
-
success: true,
|
|
258
|
-
output: t("plan.completed_archived", {
|
|
259
|
-
id: plan.id,
|
|
260
|
-
done: String(p.settled),
|
|
261
|
-
total: String(plan.steps.length),
|
|
262
|
-
}),
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
// Idempotency: re-marking a done step as done usually means the
|
|
266
|
-
// plan was already auto-advanced (all step files exist) and the
|
|
267
|
-
// model is confirming its own work. Return SUCCESS so the loop
|
|
268
|
-
// isn't confused — a small model misreads success:false as a
|
|
269
|
-
// failed action and retries/spins. Genuine spinning (repeated
|
|
270
|
-
// identical calls without new work) is still caught by the
|
|
271
|
-
// stuck-detector's consecutive-identical-call check.
|
|
272
|
-
if (status === "done" && target.status === "done") {
|
|
273
|
-
const progress = tracker.getProgressString();
|
|
274
|
-
const display = tracker.toPromptBlock();
|
|
275
|
-
return {
|
|
276
|
-
success: true,
|
|
277
|
-
output: `${t("plan.step_already_done", {
|
|
278
|
-
step: String(stepId),
|
|
279
|
-
})}\n${progress}`,
|
|
280
|
-
display,
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
// Order enforcement: only the CURRENT step may be marked done.
|
|
284
|
-
// Prevents jumping ahead (step 5 marked done before steps 1-4,
|
|
285
|
-
// observed in the post-mortem session) and the follow-on audit
|
|
286
|
-
// failures ("5/5 steps done, 5 files missing").
|
|
287
|
-
if (status === "done") {
|
|
288
|
-
const blockers = tracker
|
|
289
|
-
.getPlan()
|
|
290
|
-
.steps.filter((s) => s.id < stepId && s.status !== "done" && s.status !== "skipped");
|
|
291
|
-
if (blockers.length > 0) {
|
|
292
|
-
return {
|
|
293
|
-
success: false,
|
|
294
|
-
output: t("plan.order_blocked", {
|
|
295
|
-
step: String(stepId),
|
|
296
|
-
first: String(blockers[0].id),
|
|
297
|
-
desc: blockers[0].description,
|
|
298
|
-
}),
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
// Deliverable gate: marking a step done must match its declared
|
|
303
|
-
// intent. "create" (default) → named files must exist; "delete"
|
|
304
|
-
// → named files must be GONE. Intent is model-declared (kind),
|
|
305
|
-
// never keyword-inferred (rule #10).
|
|
306
|
-
//
|
|
307
|
-
// Auto-switch: when kind is "create" but ALL named files are gone,
|
|
308
|
-
// check if the agent actually deleted them (via delete_file/bash rm).
|
|
309
|
-
// Only then switch to "delete" — files that were never created
|
|
310
|
-
// (create-kind steps) remain blocked so the agent must create them.
|
|
311
|
-
if (status === "done" && target.kind !== "delete") {
|
|
312
|
-
const missing = deps.missingStepDeliverables(target);
|
|
313
|
-
if (missing.length > 0) {
|
|
314
|
-
const existing = deps.stillExistingDeliverables(target);
|
|
315
|
-
const allGone = deps.hasStepDeliverables(target) && existing.length === 0;
|
|
316
|
-
if (allGone) {
|
|
317
|
-
// Confirm the agent actually deleted these files (not just
|
|
318
|
-
// never created them) before auto-switching.
|
|
319
|
-
const wasDeleted = deps.wasFileDeleted
|
|
320
|
-
? missing.every((f) => deps.wasFileDeleted(f))
|
|
321
|
-
: false;
|
|
322
|
-
if (wasDeleted) {
|
|
323
|
-
target.kind = "delete";
|
|
324
|
-
deps.store.saveActive(tracker.getPlan());
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
if (status === "done") {
|
|
330
|
-
if (target.kind === "delete") {
|
|
331
|
-
const leftovers = deps.stillExistingDeliverables(target);
|
|
332
|
-
if (leftovers.length > 0) {
|
|
333
|
-
return {
|
|
334
|
-
success: false,
|
|
335
|
-
output: t("plan.deliverables_remain", {
|
|
336
|
-
step: String(stepId),
|
|
337
|
-
files: leftovers.join(", "),
|
|
338
|
-
}),
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
else {
|
|
343
|
-
const missing = deps.missingStepDeliverables(target);
|
|
344
|
-
if (missing.length > 0) {
|
|
345
|
-
return {
|
|
346
|
-
success: false,
|
|
347
|
-
output: t("plan.deliverables_missing", {
|
|
348
|
-
step: String(stepId),
|
|
349
|
-
files: missing.join(", "),
|
|
350
|
-
}),
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
// Typecheck gate: "done" while the last edit still fails to compile
|
|
356
|
-
// is a self-certification the harness must refuse (observed: steps
|
|
357
|
-
// marked done with the project typecheck failing on every write —
|
|
358
|
-
// a "fix it later" cascade that never got fixed). The model must
|
|
359
|
-
// either fix the errors (a clean re-edit clears the map) or mark
|
|
360
|
-
// the step skipped.
|
|
361
|
-
if (status === "done") {
|
|
362
|
-
const blocked = stepTypecheckGate(deps.typecheckFailures, target.description);
|
|
363
|
-
if (blocked) {
|
|
364
|
-
return {
|
|
365
|
-
success: false,
|
|
366
|
-
output: t("plan.typecheck_failed", {
|
|
367
|
-
step: String(stepId),
|
|
368
|
-
error: blocked,
|
|
369
|
-
}),
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
tracker.updateStepStatus(stepId, status);
|
|
374
|
-
if (args.note)
|
|
375
|
-
tracker.addNote(Number(args.step), String(args.note));
|
|
376
|
-
tracker.syncCurrentStep();
|
|
377
|
-
deps.store.saveActive(tracker.getPlan());
|
|
378
|
-
// A "done" step that names no file tokens has a vacuous gate — its
|
|
379
|
-
// completion cannot be auto-verified. Inform the model so it can
|
|
380
|
-
// run an explicit check or add file paths to the step description.
|
|
381
|
-
const vacuousGate = status === "done" && !deps.hasStepDeliverables(target);
|
|
382
|
-
const vacuousNote = vacuousGate
|
|
383
|
-
? `\n${t("plan.no_deliverables", { step: String(stepId) })}`
|
|
384
|
-
: "";
|
|
385
|
-
// If this update completed the last step → auto-archive. This breaks
|
|
386
|
-
// the loop where a finished plan stays active and anchors the model
|
|
387
|
-
// to "continue step N, do NOT create a new plan".
|
|
388
|
-
if (tracker.isComplete()) {
|
|
389
|
-
const plan = tracker.getPlan();
|
|
390
|
-
deps.store.archivePlan(plan);
|
|
391
|
-
deps.recordCompleted(plan); // final audit still verifies it
|
|
392
|
-
deps.trackerRef.current = null;
|
|
393
|
-
const p = getPlanProgress(plan);
|
|
394
|
-
return {
|
|
395
|
-
success: true,
|
|
396
|
-
output: `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })}\n${t("plan.completed_archived", {
|
|
397
|
-
id: plan.id,
|
|
398
|
-
done: String(p.settled),
|
|
399
|
-
total: String(plan.steps.length),
|
|
400
|
-
})}${vacuousNote}`,
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
const progress = tracker.getProgressString();
|
|
404
|
-
const display = tracker.toPromptBlock();
|
|
405
|
-
_ctx.sessionLog?.plan("step-update", `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })} | ${progress} | current: step ${tracker.getCurrentStepIndex() + 1}`);
|
|
406
|
-
return {
|
|
407
|
-
success: true,
|
|
408
|
-
output: `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })}\n${progress}${vacuousNote}`,
|
|
409
|
-
display,
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
if (action === "update" &&
|
|
413
|
-
Array.isArray(args.steps) &&
|
|
414
|
-
args.steps.length > 0 &&
|
|
415
|
-
!args.step) {
|
|
416
|
-
const tracker = deps.trackerRef.current;
|
|
417
|
-
if (!tracker) {
|
|
418
|
-
return { success: false, output: t("plan.no_active") };
|
|
419
|
-
}
|
|
420
|
-
// Rebuilding a plan that already has progress silently discards
|
|
421
|
-
// every status (ses_mt4fn58c: the model sent its whole step list
|
|
422
|
-
// with ✅-emoji statuses expecting progress marks and got a fresh
|
|
423
|
-
// plan back). A not-yet-started plan is safe to rebuild.
|
|
424
|
-
const progressed = tracker.getPlan().steps.some((s) => s.status !== "pending");
|
|
425
|
-
if (progressed) {
|
|
426
|
-
return { success: false, output: t("plan.bulk_update_rejected") };
|
|
427
|
-
}
|
|
428
|
-
const title = String(args.title || tracker.getPlan().title);
|
|
429
|
-
const steps = args.steps.map(String);
|
|
430
|
-
const parsed = deps.parseKinds(args, steps);
|
|
431
|
-
if (parsed.error) {
|
|
432
|
-
return { success: false, output: parsed.error };
|
|
433
|
-
}
|
|
434
|
-
const plan = PlanCreator.createPlan(title, steps, deps.baseDir, parsed.kinds ?? []);
|
|
435
|
-
deps.setPlan(plan);
|
|
436
|
-
const display = deps.trackerRef.current?.toPromptBlock();
|
|
437
|
-
const output = t("plan.updated", {
|
|
438
|
-
title,
|
|
439
|
-
steps: String(steps.length),
|
|
440
|
-
});
|
|
441
|
-
return {
|
|
442
|
-
success: true,
|
|
443
|
-
output,
|
|
444
|
-
display,
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
if (action === "abort") {
|
|
448
|
-
const planId = args.id ? String(args.id) : "";
|
|
449
|
-
if (planId) {
|
|
450
|
-
// Abort a SPECIFIC plan (not the active one). Draft → archive,
|
|
451
|
-
// active → archive + clear tracker. Archived plans are already
|
|
452
|
-
// terminal — there is nothing to abort.
|
|
453
|
-
const found = deps.store.find(planId);
|
|
454
|
-
if (!found) {
|
|
455
|
-
return {
|
|
456
|
-
success: false,
|
|
457
|
-
output: t("plan.not_found", { id: planId }),
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
if (found.status === "archived") {
|
|
461
|
-
return {
|
|
462
|
-
success: false,
|
|
463
|
-
output: t("plan.already_archived", { id: planId }),
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
deps.store.archivePlan(found.plan); // clears active if it was active
|
|
467
|
-
if (found.status === "active") {
|
|
468
|
-
deps.trackerRef.current = null;
|
|
469
|
-
deps.clearCompleted();
|
|
470
|
-
}
|
|
471
|
-
return {
|
|
472
|
-
success: true,
|
|
473
|
-
output: t("plan.aborted_id", { id: found.plan.id }),
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
const tracker = deps.trackerRef.current;
|
|
477
|
-
if (!tracker) {
|
|
478
|
-
// Abort with no active plan and no id — previously returned
|
|
479
|
-
// success:true, which made the model believe it aborted a plan
|
|
480
|
-
// that did not exist (observed: 3× abort on an empty list).
|
|
481
|
-
return { success: false, output: t("plan.no_active") };
|
|
482
|
-
}
|
|
483
|
-
deps.store.archivePlan(tracker.getPlan());
|
|
484
|
-
deps.trackerRef.current = null;
|
|
485
|
-
deps.store.clearActive();
|
|
486
|
-
deps.clearCompleted();
|
|
487
|
-
return { success: true, output: t("plan.aborted") };
|
|
488
|
-
}
|
|
489
|
-
if (action === "delete") {
|
|
490
|
-
const planId = String(args.id || "");
|
|
491
|
-
if (!planId) {
|
|
492
|
-
return { success: false, output: t("plan.delete_no_id") };
|
|
493
|
-
}
|
|
494
|
-
const deleted = deps.store.deletePlan(planId);
|
|
495
|
-
if (!deleted) {
|
|
496
|
-
return {
|
|
497
|
-
success: false,
|
|
498
|
-
output: t("plan.not_found", { id: planId }),
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
if (deleted === "active") {
|
|
502
|
-
deps.trackerRef.current = null;
|
|
503
|
-
deps.clearCompleted();
|
|
504
|
-
}
|
|
505
|
-
return {
|
|
506
|
-
success: true,
|
|
507
|
-
output: t("plan.deleted", { id: planId }),
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
if (action === "purge") {
|
|
511
|
-
const count = deps.store.purgeAll();
|
|
512
|
-
deps.trackerRef.current = null;
|
|
513
|
-
deps.clearCompleted();
|
|
514
|
-
return {
|
|
515
|
-
success: true,
|
|
516
|
-
output: t("plan.purged", { count: String(count) }),
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
if (!deps.trackerRef.current) {
|
|
520
|
-
// Update with an id that points at a draft/archived plan: say the
|
|
521
|
-
// plan is read-only instead of a bare "No active plan" (observed:
|
|
522
|
-
// 10+ iterations re-updating an archived plan and getting the same
|
|
523
|
-
// unhelpful answer back).
|
|
524
|
-
if (args.id) {
|
|
525
|
-
const found = deps.store.find(String(args.id));
|
|
526
|
-
if (found && found.status !== "active") {
|
|
527
|
-
return {
|
|
528
|
-
success: false,
|
|
529
|
-
output: t("plan.readonly", { id: found.plan.id, status: found.status }),
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
return { success: false, output: t("plan.no_active") };
|
|
534
|
-
}
|
|
49
|
+
// Диспетчер по действию: по одной функции на action (plan-actions.ts).
|
|
50
|
+
const result = dispatchPlanAction(action, deps, ctx, args);
|
|
51
|
+
if (result !== null)
|
|
52
|
+
return result;
|
|
535
53
|
return {
|
|
536
54
|
success: false,
|
|
537
55
|
output: t("plan.unknown_action", { action }),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeInputFrom } from "./input-from";
|
|
1
2
|
export function validatePlan(plan, config) {
|
|
2
3
|
const errors = [];
|
|
3
4
|
const warnings = [];
|
|
@@ -18,6 +19,9 @@ export function validatePlan(plan, config) {
|
|
|
18
19
|
errors.push(overlap.error);
|
|
19
20
|
}
|
|
20
21
|
}
|
|
22
|
+
const inputRefErrors = validateInputFromRefs(plan.subtasks);
|
|
23
|
+
errors.push(...inputRefErrors.errors);
|
|
24
|
+
autoFixes.push(...inputRefErrors.autoFixes);
|
|
21
25
|
return {
|
|
22
26
|
valid: errors.length === 0,
|
|
23
27
|
errors,
|
|
@@ -25,6 +29,26 @@ export function validatePlan(plan, config) {
|
|
|
25
29
|
autoFixes,
|
|
26
30
|
};
|
|
27
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Every input_from reference must point at an existing subtask and be backed
|
|
34
|
+
* by a depends_on edge — otherwise the reader may run before its producer.
|
|
35
|
+
*/
|
|
36
|
+
export function validateInputFromRefs(subtasks) {
|
|
37
|
+
const errors = [];
|
|
38
|
+
const autoFixes = [];
|
|
39
|
+
const ids = new Set(subtasks.map((s) => s.id));
|
|
40
|
+
for (const s of subtasks) {
|
|
41
|
+
for (const ref of normalizeInputFrom(s.input_from)) {
|
|
42
|
+
if (!ids.has(ref.subtaskId)) {
|
|
43
|
+
errors.push(`Subtask "${s.id}" input_from references unknown subtask "${ref.subtaskId}". Known subtasks: ${Array.from(ids).join(", ")}`);
|
|
44
|
+
}
|
|
45
|
+
else if (!s.depends_on?.includes(ref.subtaskId)) {
|
|
46
|
+
autoFixes.push({ subtaskId: s.id, fix: `Add depends_on: ${ref.subtaskId}` });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { errors, autoFixes };
|
|
51
|
+
}
|
|
28
52
|
export function expertTagsExist(subtasks, config) {
|
|
29
53
|
const errors = [];
|
|
30
54
|
const knownExperts = new Set(Object.keys(config.experts || {}));
|
|
@@ -1,18 +1,5 @@
|
|
|
1
1
|
import { t } from "../../i18n/index";
|
|
2
|
-
|
|
3
|
-
/timeout/i,
|
|
4
|
-
/timed out/i,
|
|
5
|
-
/ECONNREFUSED/i,
|
|
6
|
-
/ETIMEDOUT/i,
|
|
7
|
-
/ECONNRESET/i,
|
|
8
|
-
/killed/i,
|
|
9
|
-
/SIGTERM/i,
|
|
10
|
-
/SIGKILL/i,
|
|
11
|
-
/rate limit/i,
|
|
12
|
-
/429/i,
|
|
13
|
-
/503/i,
|
|
14
|
-
/502/i,
|
|
15
|
-
];
|
|
2
|
+
import { isTransientError } from "./transient-error";
|
|
16
3
|
const BLOCKING_PATTERNS = [
|
|
17
4
|
/EACCES/i,
|
|
18
5
|
/permission denied/i,
|
|
@@ -25,10 +12,8 @@ const BLOCKING_PATTERNS = [
|
|
|
25
12
|
/403/i,
|
|
26
13
|
];
|
|
27
14
|
export function classifyError(output) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return "transient";
|
|
31
|
-
}
|
|
15
|
+
if (isTransientError(output))
|
|
16
|
+
return "transient";
|
|
32
17
|
for (const p of BLOCKING_PATTERNS) {
|
|
33
18
|
if (p.test(output))
|
|
34
19
|
return "blocking";
|
|
@@ -28,12 +28,21 @@ export class PlanTracker {
|
|
|
28
28
|
step.note = note;
|
|
29
29
|
}
|
|
30
30
|
advance() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
// Move past any already-settled (skipped/done) steps without mutating
|
|
32
|
+
// them — overwriting a "skipped" step with "in_progress" deadlocks the
|
|
33
|
+
// plan (the model will never work on a step it deliberately skipped).
|
|
34
|
+
let idx = this.currentStepIndex + 1;
|
|
35
|
+
while (idx < this.plan.steps.length &&
|
|
36
|
+
(this.plan.steps[idx].status === "skipped" || this.plan.steps[idx].status === "done")) {
|
|
37
|
+
idx++;
|
|
38
|
+
}
|
|
39
|
+
if (idx >= this.plan.steps.length)
|
|
40
|
+
return false;
|
|
41
|
+
this.currentStepIndex = idx;
|
|
42
|
+
if (this.plan.steps[idx].status === "pending") {
|
|
43
|
+
this.plan.steps[idx].status = "in_progress";
|
|
35
44
|
}
|
|
36
|
-
return
|
|
45
|
+
return true;
|
|
37
46
|
}
|
|
38
47
|
/**
|
|
39
48
|
* Advance the pointer past any done/skipped steps without mutating their
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Единая классификация транзиентных (временно сбойных) ошибок.
|
|
3
|
+
*
|
|
4
|
+
* Ранее существовало ДВЕ расходящиеся копии: `TRANSIENT_PATTERNS` в
|
|
5
|
+
* stuck-detector.ts и `TRANSIENT_ERROR_PATTERNS` в moe-executor.ts. Из-за
|
|
6
|
+
* этого одна и та же ошибка по-разному ретраилась в разных подсистемах:
|
|
7
|
+
* moe-executor считал транзиентными 500/504/"network"/"fetch failed",
|
|
8
|
+
* stuck-detector — "killed"/"SIGTERM"/"rate limit" и т.д.
|
|
9
|
+
*
|
|
10
|
+
* Здесь — объединение обеих копий в единый источник истины, чтобы решение
|
|
11
|
+
* "эту ошибку можно ретраить" было одинаковым во всей системе.
|
|
12
|
+
*/
|
|
13
|
+
const TRANSIENT_PATTERNS = [
|
|
14
|
+
/timeout/i,
|
|
15
|
+
/timed out/i,
|
|
16
|
+
/ECONNREFUSED/i,
|
|
17
|
+
/ETIMEDOUT/i,
|
|
18
|
+
/ECONNRESET/i,
|
|
19
|
+
/killed/i,
|
|
20
|
+
/SIGTERM/i,
|
|
21
|
+
/SIGKILL/i,
|
|
22
|
+
/rate limit/i,
|
|
23
|
+
/\b(429|500|502|503|504)\b/,
|
|
24
|
+
/network/i,
|
|
25
|
+
/fetch failed/i,
|
|
26
|
+
];
|
|
27
|
+
/** Истинно, если сообщение похоже на транзиентную (временно сбойную) ошибку. */
|
|
28
|
+
export function isTransientError(message) {
|
|
29
|
+
return TRANSIENT_PATTERNS.some((p) => p.test(message));
|
|
30
|
+
}
|