micro-models-agent 0.63.0 → 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 +174 -0
- 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 +8 -4
- package/dist/i18n/ru.json +8 -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 +1755 -841
- 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/bridge-server.mjs +37 -4
- 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
|
@@ -6,6 +6,7 @@ import { Auditor, findExistingFile } from "./auditor";
|
|
|
6
6
|
import { PlanStore } from "./plan-store";
|
|
7
7
|
import { getMessageText } from "../../llm/provider";
|
|
8
8
|
import { extractFileLikeTokens, stripUrls } from "../hallucination/js-identifiers";
|
|
9
|
+
import { toForwardSlash } from "../../tools/path-utils";
|
|
9
10
|
import { existsSync, readFileSync } from "fs";
|
|
10
11
|
import { resolve } from "path";
|
|
11
12
|
import { performWebSearch } from "../../tools/web-search";
|
|
@@ -19,6 +20,63 @@ import { createExecutionPlugin, STUCK_RECOVERY_COOLDOWN, STUCK_WARN_REPEAT_EVERY
|
|
|
19
20
|
* time-based. Injectable via the constructor for tests (0 disables it).
|
|
20
21
|
*/
|
|
21
22
|
const ERROR_SEARCH_MIN_INTERVAL_MS = 30000;
|
|
23
|
+
/**
|
|
24
|
+
* Tools that may run while a plan step is active WITHOUT a step-alignment
|
|
25
|
+
* check. They either cannot touch the filesystem, or are structural/read-only
|
|
26
|
+
* inspection tools the agent legitimately needs at any point (diagnostics,
|
|
27
|
+
* index/search, memory, process introspection, interactive prompts). Only
|
|
28
|
+
* file-mutating tools are pinned to the current step's file tokens.
|
|
29
|
+
*
|
|
30
|
+
* `lsp_check` and `project_map` belong here explicitly: both are alwaysOn and
|
|
31
|
+
* the system prompt MANDATES `lsp_check` after every write (including rewrites
|
|
32
|
+
* of files owned by already-completed steps). Leaving them off the list made
|
|
33
|
+
* every diagnostic call burn a plan warning and could hard-block a legitimate
|
|
34
|
+
* write on the 3rd call (observed: "3 вызовов подряд вне текущего шага" while
|
|
35
|
+
* the agent was writing the current step's own deliverable).
|
|
36
|
+
*/
|
|
37
|
+
const PLAN_ALIGNMENT_EXEMPT_TOOLS = new Set([
|
|
38
|
+
// structural / execution control
|
|
39
|
+
"plan",
|
|
40
|
+
"todo",
|
|
41
|
+
"verify",
|
|
42
|
+
"enable_tools",
|
|
43
|
+
// filesystem reads
|
|
44
|
+
"list_dir",
|
|
45
|
+
"read_file",
|
|
46
|
+
"glob",
|
|
47
|
+
"grep",
|
|
48
|
+
"file_info",
|
|
49
|
+
// read-only diagnostics + code index
|
|
50
|
+
"lsp_check",
|
|
51
|
+
"project_map",
|
|
52
|
+
"chunk_query",
|
|
53
|
+
// web / memory / session reads
|
|
54
|
+
"web_search",
|
|
55
|
+
"web_fetch",
|
|
56
|
+
"web_browse",
|
|
57
|
+
"recall",
|
|
58
|
+
"search_history",
|
|
59
|
+
"session_info",
|
|
60
|
+
// process introspection
|
|
61
|
+
"process_list",
|
|
62
|
+
"process_log",
|
|
63
|
+
// interactive prompts (no filesystem mutation)
|
|
64
|
+
"question",
|
|
65
|
+
"approve",
|
|
66
|
+
// skills
|
|
67
|
+
"load_skill",
|
|
68
|
+
]);
|
|
69
|
+
/**
|
|
70
|
+
* Canonical form of a file token for plan alignment: forward slashes + lower
|
|
71
|
+
* case. Model tool calls pass absolute Windows paths
|
|
72
|
+
* (`C:\…\test\src\main.ts`) while plan steps name files relatively
|
|
73
|
+
* (`src/main.ts`); comparing the raw strings made every on-track write look
|
|
74
|
+
* off-path (backslash vs forward slash never intersects). Normalizing both
|
|
75
|
+
* sides lets the substring/`endsWith` overlap below match.
|
|
76
|
+
*/
|
|
77
|
+
function planPathToken(p) {
|
|
78
|
+
return toForwardSlash(p).toLowerCase();
|
|
79
|
+
}
|
|
22
80
|
export class ExecutionModule {
|
|
23
81
|
name = "execution";
|
|
24
82
|
tracker = null;
|
|
@@ -227,6 +285,34 @@ export class ExecutionModule {
|
|
|
227
285
|
* Final audit before the agent may declare the task done. Returns null when
|
|
228
286
|
* no plan is active; otherwise checks plan completion + artifact existence.
|
|
229
287
|
*/
|
|
288
|
+
/**
|
|
289
|
+
* Evidence-based auto-close of pending plan steps whose deliverables exist
|
|
290
|
+
* on disk. Small models routinely finish the actual work but forget the
|
|
291
|
+
* `plan update` bookkeeping (observed ses_mthn3c2a: 2/5 steps marked done
|
|
292
|
+
* while every artifact existed — the audit gate then rejected a correct
|
|
293
|
+
* final answer and burned all retry budget on bookkeeping, not on work).
|
|
294
|
+
*
|
|
295
|
+
* Standard is the SAME one the auditor already applies to done steps: every
|
|
296
|
+
* file-like token in the step description resolves to an existing file.
|
|
297
|
+
* Steps without any file token stay pending (nothing to verify).
|
|
298
|
+
*/
|
|
299
|
+
autoCloseEvidenceSteps(plan) {
|
|
300
|
+
let closed = 0;
|
|
301
|
+
for (const step of plan.steps) {
|
|
302
|
+
if (step.status === "done" || step.status === "skipped")
|
|
303
|
+
continue;
|
|
304
|
+
const tokens = extractFileLikeTokens(stripUrls(step.description));
|
|
305
|
+
if (tokens.length === 0)
|
|
306
|
+
continue;
|
|
307
|
+
const allExist = tokens.every((f) => findExistingFile(this.baseDir, f) !== null);
|
|
308
|
+
if (!allExist)
|
|
309
|
+
continue;
|
|
310
|
+
step.status = "done";
|
|
311
|
+
step.note = `auto-closed by final audit: all named files exist (${tokens.join(", ")})`;
|
|
312
|
+
closed++;
|
|
313
|
+
}
|
|
314
|
+
return closed;
|
|
315
|
+
}
|
|
230
316
|
async runFinalAudit() {
|
|
231
317
|
// No active tracker: audit the last completed plan instead of bailing.
|
|
232
318
|
// A plan finished via `plan update` auto-archives and clears the tracker;
|
|
@@ -236,9 +322,13 @@ export class ExecutionModule {
|
|
|
236
322
|
if (!this.completedPlan)
|
|
237
323
|
return null;
|
|
238
324
|
const plan = this.completedPlan;
|
|
325
|
+
const autoClosed = this.autoCloseEvidenceSteps(plan);
|
|
239
326
|
const audit = await this.auditor.audit(plan);
|
|
240
327
|
const pendingSteps = plan.steps.flatMap((s) => s.status !== "done" && s.status !== "skipped" ? [`${s.id}. ${s.description}`] : []);
|
|
241
328
|
const done = plan.steps.filter((s) => s.status === "done").length;
|
|
329
|
+
if (autoClosed > 0) {
|
|
330
|
+
this.store.saveActive(plan);
|
|
331
|
+
}
|
|
242
332
|
return {
|
|
243
333
|
passed: audit.passed && pendingSteps.length === 0,
|
|
244
334
|
done,
|
|
@@ -253,6 +343,10 @@ export class ExecutionModule {
|
|
|
253
343
|
return null;
|
|
254
344
|
}
|
|
255
345
|
const plan = this.tracker.getPlan();
|
|
346
|
+
const autoClosed = this.autoCloseEvidenceSteps(plan);
|
|
347
|
+
if (autoClosed > 0) {
|
|
348
|
+
this.store.saveActive(plan);
|
|
349
|
+
}
|
|
256
350
|
const audit = await this.auditor.audit(plan);
|
|
257
351
|
const pendingSteps = plan.steps.flatMap((s) => s.status !== "done" && s.status !== "skipped" ? [`${s.id}. ${s.description}`] : []);
|
|
258
352
|
const done = plan.steps.filter((s) => s.status === "done").length;
|
|
@@ -349,20 +443,9 @@ export class ExecutionModule {
|
|
|
349
443
|
const step = this.tracker.getCurrentStep();
|
|
350
444
|
if (!step)
|
|
351
445
|
return null;
|
|
352
|
-
|
|
353
|
-
"plan",
|
|
354
|
-
"todo",
|
|
355
|
-
"verify",
|
|
356
|
-
"list_dir",
|
|
357
|
-
"read_file",
|
|
358
|
-
"glob",
|
|
359
|
-
"grep",
|
|
360
|
-
"file_info",
|
|
361
|
-
"load_skill",
|
|
362
|
-
];
|
|
363
|
-
if (allowedAlways.includes(call.name))
|
|
446
|
+
if (PLAN_ALIGNMENT_EXEMPT_TOOLS.has(call.name))
|
|
364
447
|
return null;
|
|
365
|
-
const stepPaths = extractFileLikeTokens(stripUrls(step.description)).map(
|
|
448
|
+
const stepPaths = extractFileLikeTokens(stripUrls(step.description)).map(planPathToken);
|
|
366
449
|
if (stepPaths.length === 0)
|
|
367
450
|
return null;
|
|
368
451
|
// Only path-ish arguments are inspected. JSON-stringifying the whole args
|
|
@@ -370,7 +453,7 @@ export class ExecutionModule {
|
|
|
370
453
|
// off-path check (e.g. `import "./styles.css"` inside the file body) and
|
|
371
454
|
// raised phantom alignment warnings for on-track writes.
|
|
372
455
|
const argStr = this.pathArgStrings(call.arguments).join(" ");
|
|
373
|
-
const callPaths = extractFileLikeTokens(stripUrls(argStr)).map(
|
|
456
|
+
const callPaths = extractFileLikeTokens(stripUrls(argStr)).map(planPathToken);
|
|
374
457
|
if (callPaths.length === 0)
|
|
375
458
|
return null;
|
|
376
459
|
// Files that already belong to COMPLETED (done/skipped) steps are
|
|
@@ -381,15 +464,21 @@ export class ExecutionModule {
|
|
|
381
464
|
for (const s of plan.steps) {
|
|
382
465
|
if (s.status === "done" || s.status === "skipped") {
|
|
383
466
|
extractFileLikeTokens(stripUrls(s.description))
|
|
384
|
-
.map(
|
|
467
|
+
.map(planPathToken)
|
|
385
468
|
.forEach((p) => finishedPaths.add(p));
|
|
386
469
|
}
|
|
387
470
|
}
|
|
471
|
+
// Bidirectional path-token overlap, used for BOTH checks below. A step may
|
|
472
|
+
// name "main.ts" while the tool calls "src/main.ts" (or vice versa), so a
|
|
473
|
+
// one-directional `s.includes(p)` missed the common nested/partial case
|
|
474
|
+
// (observed: rewriting a completed step's src/main.ts was flagged as
|
|
475
|
+
// off-path for the current step and helped hard-block the third call).
|
|
476
|
+
const matchesAny = (p, tokens) => tokens.some((t) => t === p || t.includes(p) || p.includes(t));
|
|
477
|
+
const finished = [...finishedPaths];
|
|
388
478
|
const offPath = callPaths.some((p) => {
|
|
389
|
-
if (
|
|
479
|
+
if (matchesAny(p, finished))
|
|
390
480
|
return false;
|
|
391
|
-
|
|
392
|
-
return !stepPaths.some((s) => p.includes(s) || s.includes(p));
|
|
481
|
+
return !matchesAny(p, stepPaths);
|
|
393
482
|
});
|
|
394
483
|
if (!offPath)
|
|
395
484
|
return null;
|
|
@@ -3,27 +3,34 @@ import { filterToolsByTags } from "../../tools/filter-tools";
|
|
|
3
3
|
import { StuckDetector } from "./stuck-detector";
|
|
4
4
|
import { t } from "../../i18n/index";
|
|
5
5
|
import { ArtifactStore } from "../artifacts/store";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
"fetch failed",
|
|
19
|
-
"abort",
|
|
20
|
-
"Abort",
|
|
21
|
-
];
|
|
22
|
-
function isTransientError(error) {
|
|
23
|
-
return TRANSIENT_ERROR_PATTERNS.some((p) => error.includes(p));
|
|
6
|
+
import { buildInputSection } from "./input-from";
|
|
7
|
+
import { parseScopeRequest } from "../../tools/scope-request";
|
|
8
|
+
import { isTransientError } from "./transient-error";
|
|
9
|
+
import { sleep } from "../../utils/sleep";
|
|
10
|
+
import { backoffDelay } from "../../utils/retry";
|
|
11
|
+
import { errMsg } from "../../utils";
|
|
12
|
+
export const MAX_SCOPE_EXPANSION_CYCLES = 2;
|
|
13
|
+
/** Uniform interrupt error recognized by the agent loop (err.name === "AbortError"). */
|
|
14
|
+
export function newAbortError() {
|
|
15
|
+
const e = new Error(t("tool.aborted", { name: "MoE execution" }));
|
|
16
|
+
e.name = "AbortError";
|
|
17
|
+
return e;
|
|
24
18
|
}
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Сборка детального сообщения об ошибке подзадачи: базовый текст + подсказки
|
|
21
|
+
* stuck-детектора + совет про load_skill. Дублировался 2× в executeSubtask.
|
|
22
|
+
*/
|
|
23
|
+
function buildErrorDetail(stuckDetector, baseError, lastError) {
|
|
24
|
+
const hints = stuckDetector.getActionableHints();
|
|
25
|
+
let errorDetail = baseError;
|
|
26
|
+
if (hints.length > 0) {
|
|
27
|
+
errorDetail += `\n\n${t("exec.hints", { hints: hints.map((h) => `- ${h}`).join("\n") })}`;
|
|
28
|
+
}
|
|
29
|
+
if (lastError) {
|
|
30
|
+
errorDetail +=
|
|
31
|
+
"\n\nIf relevant skills are available, consider loading one with load_skill for expert guidance.";
|
|
32
|
+
}
|
|
33
|
+
return errorDetail;
|
|
27
34
|
}
|
|
28
35
|
export function topologicalSort(subtasks) {
|
|
29
36
|
const sorted = [];
|
|
@@ -48,7 +55,7 @@ export function topologicalSort(subtasks) {
|
|
|
48
55
|
}
|
|
49
56
|
return sorted;
|
|
50
57
|
}
|
|
51
|
-
async function executeSubtask(subtask, deps,
|
|
58
|
+
async function executeSubtask(subtask, deps, depResults, sharedContext, onUsage) {
|
|
52
59
|
const startTime = Date.now();
|
|
53
60
|
const expertConfig = getExpertConfig(deps.config, subtask.expert_tag);
|
|
54
61
|
if (!expertConfig) {
|
|
@@ -95,7 +102,11 @@ async function executeSubtask(subtask, deps, _sharedContext) {
|
|
|
95
102
|
const scopePrompt = allowedFiles.length > 0 || readOnlyFiles.length > 0
|
|
96
103
|
? `\n\nAllowed files to write: ${allowedFiles.join(", ") || "(none)"}\nRead-only files: ${readOnlyFiles.join(", ") || "(none)"}`
|
|
97
104
|
: "";
|
|
98
|
-
const
|
|
105
|
+
const sharedRules = sharedContext?.rules;
|
|
106
|
+
const sharedContextPrompt = sharedRules && sharedRules.length > 0
|
|
107
|
+
? `\n\nShared rules:\n${sharedRules.map((r) => `- ${r}`).join("\n")}`
|
|
108
|
+
: "";
|
|
109
|
+
const taskPrompt = `${subtask.description}\n\nExpected output: ${subtask.expected_output}\nSuccess criteria:\n${(subtask.success_criteria || []).map((c) => `- ${c}`).join("\n")}${scopePrompt}${sharedContextPrompt}${buildInputSection(subtask, depResults, deps.config.subagent?.maxSummaryChars ?? 2000)}`;
|
|
99
110
|
const subagentArgs = {
|
|
100
111
|
task: taskPrompt,
|
|
101
112
|
allowed_files: allowedFiles,
|
|
@@ -106,22 +117,39 @@ async function executeSubtask(subtask, deps, _sharedContext) {
|
|
|
106
117
|
subagentArgs.result_mode = "file";
|
|
107
118
|
subagentArgs.artifact_name = subtask.id;
|
|
108
119
|
}
|
|
120
|
+
const subagentStart = Date.now();
|
|
121
|
+
deps.onTool?.({
|
|
122
|
+
type: "start",
|
|
123
|
+
tool: "subagent",
|
|
124
|
+
args: { subtask: subtask.id, description: subtask.description },
|
|
125
|
+
});
|
|
109
126
|
const result = await subagentTool.handler({
|
|
110
127
|
config: deps.config,
|
|
111
128
|
baseDir: deps.baseDir,
|
|
112
129
|
logger: deps.logger,
|
|
113
130
|
llmProvider: deps.llmProvider,
|
|
114
131
|
toolExecutor: deps.toolExecutor,
|
|
115
|
-
sessionId: "moe",
|
|
132
|
+
sessionId: deps.sessionId || "moe",
|
|
116
133
|
}, subagentArgs);
|
|
134
|
+
if (result.usage) {
|
|
135
|
+
onUsage?.(subtask.expert_tag, result.usage);
|
|
136
|
+
}
|
|
137
|
+
deps.onTool?.({
|
|
138
|
+
type: "end",
|
|
139
|
+
tool: "subagent",
|
|
140
|
+
args: { subtask: subtask.id, description: subtask.description },
|
|
141
|
+
duration: Date.now() - subagentStart,
|
|
142
|
+
error: !result.success,
|
|
143
|
+
});
|
|
117
144
|
if (result.success) {
|
|
118
145
|
if (deps.config.subagent?.resultMode === "file") {
|
|
119
|
-
|
|
120
|
-
|
|
146
|
+
// Structured field from the subagent tool — locale-independent
|
|
147
|
+
// (parsing the localized output string broke under ru).
|
|
148
|
+
const abs = result.artifactPath ?? "";
|
|
121
149
|
if (abs) {
|
|
122
150
|
const store = new ArtifactStore({
|
|
123
151
|
baseDir: deps.baseDir,
|
|
124
|
-
sessionId: "moe",
|
|
152
|
+
sessionId: deps.sessionId || "moe",
|
|
125
153
|
});
|
|
126
154
|
const full = store.read(abs.split(/[\\/]/).pop() || ""); // file name only, scoped to root
|
|
127
155
|
deps.logger.debug(`MoE subtask "${subtask.id}" attached artifact ${abs} (${full?.length ?? 0} chars) to Router`);
|
|
@@ -156,48 +184,38 @@ async function executeSubtask(subtask, deps, _sharedContext) {
|
|
|
156
184
|
}
|
|
157
185
|
}
|
|
158
186
|
if (isTransientError(lastError) && attempt < maxAttempts) {
|
|
159
|
-
const delay =
|
|
187
|
+
const delay = backoffDelay(attempt - 1, 1000, 8000);
|
|
160
188
|
deps.logger.debug(`Retrying subtask "${subtask.id}" (attempt ${attempt}/${maxAttempts}) after ${delay}ms: transient error`);
|
|
161
189
|
await sleep(delay);
|
|
162
190
|
continue;
|
|
163
191
|
}
|
|
164
192
|
// Non-transient failure — build detailed error with hints
|
|
165
|
-
const
|
|
166
|
-
let errorDetail = result.output;
|
|
167
|
-
if (hints.length > 0) {
|
|
168
|
-
errorDetail += `\n\n${t("exec.hints", { hints: hints.map((h) => `- ${h}`).join("\n") })}`;
|
|
169
|
-
}
|
|
170
|
-
if (lastError) {
|
|
171
|
-
errorDetail +=
|
|
172
|
-
"\n\nIf relevant skills are available, consider loading one with load_skill for expert guidance.";
|
|
173
|
-
}
|
|
193
|
+
const errorDetail = buildErrorDetail(stuckDetector, result.output, lastError);
|
|
174
194
|
return {
|
|
175
195
|
subtaskId: subtask.id,
|
|
176
196
|
success: false,
|
|
177
197
|
summary: `Failed: ${subtask.id}`,
|
|
178
198
|
result: result.output,
|
|
179
199
|
error: errorDetail,
|
|
200
|
+
scopeRequest: parseScopeRequest(result.output) ?? undefined,
|
|
180
201
|
durationMs: Date.now() - startTime,
|
|
181
202
|
};
|
|
182
203
|
}
|
|
183
204
|
catch (e) {
|
|
184
205
|
lastError = e.message;
|
|
185
206
|
stuckDetector.recordToolError("subagent", e.message);
|
|
207
|
+
// User interrupt (Esc) is NEVER transient — propagate immediately so
|
|
208
|
+
// the wave barrier and the agent loop see the abort instead of retrying.
|
|
209
|
+
if (e?.name === "AbortError") {
|
|
210
|
+
throw e;
|
|
211
|
+
}
|
|
186
212
|
if (isTransientError(lastError) && attempt < maxAttempts) {
|
|
187
|
-
const delay =
|
|
213
|
+
const delay = backoffDelay(attempt - 1, 1000, 8000);
|
|
188
214
|
deps.logger.debug(`Retrying subtask "${subtask.id}" (attempt ${attempt}/${maxAttempts}) after ${delay}ms: ${lastError}`);
|
|
189
215
|
await sleep(delay);
|
|
190
216
|
continue;
|
|
191
217
|
}
|
|
192
|
-
const
|
|
193
|
-
let errorDetail = e.message;
|
|
194
|
-
if (hints.length > 0) {
|
|
195
|
-
errorDetail += `\n\n${t("exec.hints", { hints: hints.map((h) => `- ${h}`).join("\n") })}`;
|
|
196
|
-
}
|
|
197
|
-
if (lastError) {
|
|
198
|
-
errorDetail +=
|
|
199
|
-
"\n\nIf relevant skills are available, consider loading one with load_skill for expert guidance.";
|
|
200
|
-
}
|
|
218
|
+
const errorDetail = buildErrorDetail(stuckDetector, e.message, lastError);
|
|
201
219
|
return {
|
|
202
220
|
subtaskId: subtask.id,
|
|
203
221
|
success: false,
|
|
@@ -225,14 +243,35 @@ async function executeSubtask(subtask, deps, _sharedContext) {
|
|
|
225
243
|
}
|
|
226
244
|
export class MoEExecutor {
|
|
227
245
|
deps;
|
|
246
|
+
usageByTag = new Map();
|
|
228
247
|
constructor(deps) {
|
|
229
248
|
this.deps = deps;
|
|
230
249
|
}
|
|
231
|
-
|
|
232
|
-
|
|
250
|
+
/** Aggregated token usage per expert_tag across all executed subtasks. */
|
|
251
|
+
getUsageByTag() {
|
|
252
|
+
return Object.fromEntries(this.usageByTag);
|
|
253
|
+
}
|
|
254
|
+
recordUsage(expertTag, usage) {
|
|
255
|
+
const entry = this.usageByTag.get(expertTag) ?? {
|
|
256
|
+
promptTokens: 0,
|
|
257
|
+
completionTokens: 0,
|
|
258
|
+
totalTokens: 0,
|
|
259
|
+
subtasks: 0,
|
|
260
|
+
};
|
|
261
|
+
entry.promptTokens += usage.promptTokens;
|
|
262
|
+
entry.completionTokens += usage.completionTokens;
|
|
263
|
+
entry.totalTokens += usage.totalTokens;
|
|
264
|
+
entry.subtasks += 1;
|
|
265
|
+
this.usageByTag.set(expertTag, entry);
|
|
266
|
+
}
|
|
267
|
+
async executePlan(plan, opts) {
|
|
268
|
+
const skipIds = opts?.skipIds;
|
|
269
|
+
const carried = opts?.carriedResults ?? [];
|
|
270
|
+
const results = carried.map((r) => ({ ...r }));
|
|
233
271
|
const errors = [];
|
|
234
272
|
const warnings = [];
|
|
235
|
-
const
|
|
273
|
+
const subtasks = skipIds && skipIds.size > 0 ? plan.subtasks.filter((s) => !skipIds.has(s.id)) : plan.subtasks;
|
|
274
|
+
const waves = topologicalSort(subtasks);
|
|
236
275
|
if (waves.length === 0) {
|
|
237
276
|
return {
|
|
238
277
|
success: false,
|
|
@@ -243,8 +282,8 @@ export class MoEExecutor {
|
|
|
243
282
|
}
|
|
244
283
|
// Verify all subtasks are included — catch silent drops from unresolved dependencies
|
|
245
284
|
const sortedCount = waves.flat().length;
|
|
246
|
-
if (sortedCount <
|
|
247
|
-
const missing =
|
|
285
|
+
if (sortedCount < subtasks.length) {
|
|
286
|
+
const missing = subtasks
|
|
248
287
|
.filter((s) => !waves.flat().some((w) => w.id === s.id))
|
|
249
288
|
.map((s) => s.id);
|
|
250
289
|
return {
|
|
@@ -258,6 +297,7 @@ export class MoEExecutor {
|
|
|
258
297
|
}
|
|
259
298
|
for (let waveIdx = 0; waveIdx < waves.length; waveIdx++) {
|
|
260
299
|
const wave = waves[waveIdx];
|
|
300
|
+
const depResults = new Map(results.map((r) => [r.subtaskId, r]));
|
|
261
301
|
// Cap concurrent subtasks per wave (rateLimits.maxParallelTasks, default 5).
|
|
262
302
|
const maxParallel = this.deps.config?.security?.rateLimits?.maxParallelTasks ?? 5;
|
|
263
303
|
let active = 0;
|
|
@@ -273,32 +313,104 @@ export class MoEExecutor {
|
|
|
273
313
|
};
|
|
274
314
|
const wavePromises = wave.map(async (subtask) => {
|
|
275
315
|
await acquire();
|
|
316
|
+
if (this.deps.signal?.aborted) {
|
|
317
|
+
// Checked after acquiring so subtasks queued behind an in-flight one
|
|
318
|
+
// never start once Esc was pressed mid-wave.
|
|
319
|
+
release();
|
|
320
|
+
throw newAbortError();
|
|
321
|
+
}
|
|
322
|
+
this.deps.onEvent?.({
|
|
323
|
+
type: "moe_subtask",
|
|
324
|
+
data: { subtaskId: subtask.id, expertTag: subtask.expert_tag, status: "start" },
|
|
325
|
+
});
|
|
276
326
|
let result;
|
|
277
327
|
try {
|
|
278
|
-
result = await executeSubtask(subtask, this.deps, plan.shared_context);
|
|
328
|
+
result = await executeSubtask(subtask, this.deps, depResults, plan.shared_context, (tag, usage) => this.recordUsage(tag, usage));
|
|
329
|
+
if (!result.success && this.deps.onScopeRequest) {
|
|
330
|
+
result = await this.resolveScopeAndRetry(subtask, result, depResults, plan.shared_context);
|
|
331
|
+
}
|
|
279
332
|
}
|
|
280
333
|
catch (e) {
|
|
334
|
+
if (e instanceof Error && e.name === "AbortError")
|
|
335
|
+
throw e;
|
|
281
336
|
result = {
|
|
282
337
|
subtaskId: subtask.id,
|
|
283
338
|
success: false,
|
|
284
339
|
summary: `Unhandled error: ${subtask.id}`,
|
|
285
340
|
result: "",
|
|
286
|
-
error:
|
|
341
|
+
error: errMsg(e),
|
|
287
342
|
durationMs: 0,
|
|
288
343
|
};
|
|
289
344
|
}
|
|
290
345
|
finally {
|
|
291
346
|
release();
|
|
292
347
|
}
|
|
348
|
+
this.deps.onEvent?.({
|
|
349
|
+
type: "moe_subtask",
|
|
350
|
+
data: {
|
|
351
|
+
subtaskId: subtask.id,
|
|
352
|
+
expertTag: subtask.expert_tag,
|
|
353
|
+
status: result.success ? "done" : "failed",
|
|
354
|
+
durationMs: result.durationMs,
|
|
355
|
+
summary: result.summary,
|
|
356
|
+
},
|
|
357
|
+
});
|
|
293
358
|
results.push(result);
|
|
294
359
|
return result;
|
|
295
360
|
});
|
|
296
361
|
await Promise.all(wavePromises);
|
|
362
|
+
// Wave barrier: once interrupted, stop before the next wave instead of
|
|
363
|
+
// starting new sub-agents after Esc.
|
|
364
|
+
if (this.deps.signal?.aborted)
|
|
365
|
+
throw newAbortError();
|
|
297
366
|
}
|
|
298
367
|
const failed = results.filter((r) => !r.success);
|
|
299
368
|
if (failed.length > 0) {
|
|
300
369
|
errors.push(...failed.map((f) => `[${f.subtaskId}] ${f.error || f.summary}`));
|
|
301
370
|
}
|
|
302
|
-
return {
|
|
371
|
+
return {
|
|
372
|
+
success: failed.length === 0,
|
|
373
|
+
results,
|
|
374
|
+
errors,
|
|
375
|
+
warnings,
|
|
376
|
+
...(carried.length > 0 ? { carriedSubtaskIds: carried.map((r) => r.subtaskId) } : {}),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Scope expansion protocol (spec §Post-MVP): a failed sub-agent's
|
|
381
|
+
* <scope-request> goes to the Router; approve extends the subtask scope and
|
|
382
|
+
* re-executes ONLY this subtask, reject fails it (it can still be fixed by
|
|
383
|
+
* a full re-plan). Expansion is capped — never automatic.
|
|
384
|
+
*/
|
|
385
|
+
async resolveScopeAndRetry(subtask, failed, depResults, sharedContext) {
|
|
386
|
+
let current = failed;
|
|
387
|
+
for (let cycle = 0; cycle < MAX_SCOPE_EXPANSION_CYCLES; cycle++) {
|
|
388
|
+
const req = current.scopeRequest;
|
|
389
|
+
if (!req)
|
|
390
|
+
return current;
|
|
391
|
+
const decision = await this.deps.onScopeRequest(subtask.id, req);
|
|
392
|
+
if (decision.action !== "approve") {
|
|
393
|
+
this.deps.logger.info?.(`MoE: router rejected scope request for "${subtask.id}" (${req.reason})`);
|
|
394
|
+
return {
|
|
395
|
+
...current,
|
|
396
|
+
error: `${current.error}\nRouter rejected scope request for files: ${req.files.join(", ")}`,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
subtask.allowed_files = uniq([...(subtask.allowed_files || []), ...decision.write]);
|
|
400
|
+
subtask.read_only_files = uniq([...(subtask.read_only_files || []), ...decision.read]);
|
|
401
|
+
this.deps.logger.info?.(`MoE: approved scope expansion for "${subtask.id}": +write [${decision.write.join(", ")}] +read [${decision.read.join(", ")}]`);
|
|
402
|
+
const retry = await executeSubtask(subtask, this.deps, depResults, sharedContext, (tag, usage) => this.recordUsage(tag, usage));
|
|
403
|
+
if (retry.success || !retry.scopeRequest)
|
|
404
|
+
return retry;
|
|
405
|
+
current = retry;
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
...current,
|
|
409
|
+
summary: `Failed after ${MAX_SCOPE_EXPANSION_CYCLES} scope expansions: ${subtask.id}`,
|
|
410
|
+
error: `${current.error}\nScope expansion limit (${MAX_SCOPE_EXPANSION_CYCLES}) reached`,
|
|
411
|
+
};
|
|
303
412
|
}
|
|
304
413
|
}
|
|
414
|
+
function uniq(arr) {
|
|
415
|
+
return Array.from(new Set(arr));
|
|
416
|
+
}
|