micro-models-agent 0.24.12 → 0.25.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.
Files changed (167) hide show
  1. package/dist/cli/commands.js +220 -0
  2. package/dist/cli/completer.js +168 -0
  3. package/dist/cli/index.js +2 -0
  4. package/dist/cli/main.js +113 -0
  5. package/dist/cli/repl.js +987 -0
  6. package/dist/cli/security-commands.js +166 -0
  7. package/dist/cli/setup.js +229 -0
  8. package/dist/config/config.js +186 -0
  9. package/dist/config/defaults.js +91 -0
  10. package/dist/config/experts.js +15 -0
  11. package/dist/config/index.js +3 -0
  12. package/dist/config/security.js +193 -0
  13. package/dist/config/types.js +1 -0
  14. package/dist/core/agent-moe.js +98 -0
  15. package/dist/core/agent.js +461 -0
  16. package/dist/core/bootstrap.js +321 -0
  17. package/dist/core/index.js +2 -0
  18. package/dist/core/prompt-builder.js +55 -0
  19. package/dist/core/session-logger.js +122 -0
  20. package/dist/core/types.js +1 -0
  21. package/dist/i18n/en.json +461 -0
  22. package/dist/i18n/index.js +43 -0
  23. package/dist/i18n/ru.json +461 -0
  24. package/dist/index.js +22 -0
  25. package/dist/llm/image-utils.js +144 -0
  26. package/dist/llm/index.js +4 -0
  27. package/dist/llm/model-loader.js +78 -0
  28. package/dist/llm/openai-compat.js +324 -0
  29. package/dist/llm/orchestrator.js +194 -0
  30. package/dist/llm/provider.js +10 -0
  31. package/dist/llm/response.js +39 -0
  32. package/dist/llm/token-counter.js +39 -0
  33. package/dist/llm/types.js +1 -0
  34. package/dist/logger/app-logger.js +76 -0
  35. package/dist/logger/index.js +1 -0
  36. package/dist/main.js +733 -551
  37. package/dist/migration/backup.js +45 -0
  38. package/dist/migration/detect.js +50 -0
  39. package/dist/migration/index.js +2 -0
  40. package/dist/modules/browser/actions.js +46 -0
  41. package/dist/modules/browser/cookie-store.js +24 -0
  42. package/dist/modules/browser/index.js +5 -0
  43. package/dist/modules/browser/module.js +28 -0
  44. package/dist/modules/browser/session.js +287 -0
  45. package/dist/modules/browser/snapshot.js +114 -0
  46. package/dist/modules/browser/types.js +9 -0
  47. package/dist/modules/context/history.js +15 -0
  48. package/dist/modules/context/index.js +1 -0
  49. package/dist/modules/context/manager.js +240 -0
  50. package/dist/modules/execution/auditor.js +72 -0
  51. package/dist/modules/execution/index.js +6 -0
  52. package/dist/modules/execution/module.js +337 -0
  53. package/dist/modules/execution/moe-executor.js +209 -0
  54. package/dist/modules/execution/plan-validator.js +153 -0
  55. package/dist/modules/execution/planner.js +35 -0
  56. package/dist/modules/execution/stuck-detector.js +134 -0
  57. package/dist/modules/execution/tracker.js +53 -0
  58. package/dist/modules/execution/types.js +1 -0
  59. package/dist/modules/execution/verifier.js +149 -0
  60. package/dist/modules/hallucination/confidence.js +54 -0
  61. package/dist/modules/hallucination/consistency.js +60 -0
  62. package/dist/modules/hallucination/detector.js +41 -0
  63. package/dist/modules/hallucination/factual.js +170 -0
  64. package/dist/modules/hallucination/index.js +4 -0
  65. package/dist/modules/index.js +5 -0
  66. package/dist/modules/indexer/cache.js +38 -0
  67. package/dist/modules/indexer/index.js +3 -0
  68. package/dist/modules/indexer/module.js +192 -0
  69. package/dist/modules/indexer/walker.js +101 -0
  70. package/dist/modules/mcp/client.js +393 -0
  71. package/dist/modules/mcp/index.js +3 -0
  72. package/dist/modules/mcp/module.js +146 -0
  73. package/dist/modules/mcp/registry.js +15 -0
  74. package/dist/modules/memory/index.js +1 -0
  75. package/dist/modules/memory/module.js +48 -0
  76. package/dist/modules/memory/search.js +40 -0
  77. package/dist/modules/memory/store.js +65 -0
  78. package/dist/modules/pipelines/engine.js +60 -0
  79. package/dist/modules/pipelines/index.js +3 -0
  80. package/dist/modules/pipelines/parser.js +53 -0
  81. package/dist/modules/pipelines/template.js +14 -0
  82. package/dist/modules/plugins/builtin/lint-on-write.js +121 -0
  83. package/dist/modules/plugins/builtin/notify.js +8 -0
  84. package/dist/modules/plugins/index.js +1 -0
  85. package/dist/modules/plugins/loader.js +28 -0
  86. package/dist/modules/plugins/manager.js +161 -0
  87. package/dist/modules/plugins/types.js +1 -0
  88. package/dist/modules/processes/detect.js +34 -0
  89. package/dist/modules/processes/index.js +3 -0
  90. package/dist/modules/processes/registry.js +148 -0
  91. package/dist/modules/processes/runner.js +124 -0
  92. package/dist/modules/registry.js +45 -0
  93. package/dist/modules/security/audit-log.js +116 -0
  94. package/dist/modules/security/audit-notifier.js +292 -0
  95. package/dist/modules/security/command-validator.js +185 -0
  96. package/dist/modules/security/content-scanner.js +52 -0
  97. package/dist/modules/security/data-sanitizer.js +97 -0
  98. package/dist/modules/security/encryption.js +240 -0
  99. package/dist/modules/security/index.js +14 -0
  100. package/dist/modules/security/network-validator.js +79 -0
  101. package/dist/modules/security/path-validator.js +155 -0
  102. package/dist/modules/security/rate-limiter.js +119 -0
  103. package/dist/modules/security/security-policies.js +393 -0
  104. package/dist/modules/security/session-encryption.js +193 -0
  105. package/dist/modules/security/session-isolation.js +95 -0
  106. package/dist/modules/session/index.js +3 -0
  107. package/dist/modules/session/manager.js +167 -0
  108. package/dist/modules/session/module.js +24 -0
  109. package/dist/modules/session/store.js +174 -0
  110. package/dist/modules/session/types.js +1 -0
  111. package/dist/modules/skills/index.js +3 -0
  112. package/dist/modules/skills/loader.js +72 -0
  113. package/dist/modules/skills/matcher.js +27 -0
  114. package/dist/modules/skills/module.js +143 -0
  115. package/dist/modules/types.js +1 -0
  116. package/dist/modules/updater/checker.js +32 -0
  117. package/dist/modules/updater/index.js +1 -0
  118. package/dist/modules/user-profile/compressor.js +16 -0
  119. package/dist/modules/user-profile/index.js +1 -0
  120. package/dist/modules/user-profile/profile.js +68 -0
  121. package/dist/tools/approve.js +32 -0
  122. package/dist/tools/attach-image.js +89 -0
  123. package/dist/tools/bash.js +140 -0
  124. package/dist/tools/browser.js +97 -0
  125. package/dist/tools/create-dir.js +56 -0
  126. package/dist/tools/delete-file.js +63 -0
  127. package/dist/tools/edit-file.js +77 -0
  128. package/dist/tools/executor.js +95 -0
  129. package/dist/tools/file-info.js +45 -0
  130. package/dist/tools/filter-tools.js +10 -0
  131. package/dist/tools/glob-tool.js +26 -0
  132. package/dist/tools/grep-tool.js +64 -0
  133. package/dist/tools/index.js +52 -0
  134. package/dist/tools/list-dir.js +47 -0
  135. package/dist/tools/load-skill.js +48 -0
  136. package/dist/tools/mcp-call.js +68 -0
  137. package/dist/tools/move-file.js +84 -0
  138. package/dist/tools/path-utils.js +51 -0
  139. package/dist/tools/pipeline-run.js +144 -0
  140. package/dist/tools/preview.js +2 -0
  141. package/dist/tools/process-kill.js +29 -0
  142. package/dist/tools/process-list.js +38 -0
  143. package/dist/tools/process-log.js +41 -0
  144. package/dist/tools/question.js +142 -0
  145. package/dist/tools/read-file.js +73 -0
  146. package/dist/tools/recall.js +110 -0
  147. package/dist/tools/registry.js +36 -0
  148. package/dist/tools/remember.js +67 -0
  149. package/dist/tools/scope-check.js +30 -0
  150. package/dist/tools/search-history.js +64 -0
  151. package/dist/tools/subagent.js +142 -0
  152. package/dist/tools/types.js +1 -0
  153. package/dist/tools/user-input.js +123 -0
  154. package/dist/tools/web-browse.js +57 -0
  155. package/dist/tools/web-fetch.js +72 -0
  156. package/dist/tools/web-search.js +59 -0
  157. package/dist/tools/write-file.js +80 -0
  158. package/dist/ui/box.js +81 -0
  159. package/dist/ui/colors.js +4 -0
  160. package/dist/ui/diff.js +185 -0
  161. package/dist/ui/index.js +6 -0
  162. package/dist/ui/md-formatter.js +212 -0
  163. package/dist/ui/output.js +13 -0
  164. package/dist/ui/renderer.js +141 -0
  165. package/dist/ui/spinner.js +70 -0
  166. package/dist/ui/table.js +144 -0
  167. package/package.json +1 -1
@@ -0,0 +1,240 @@
1
+ import { getMessageText } from "../../llm/provider";
2
+ const COMPACTION_INTERVAL = 15;
3
+ const KEEP_LAST_N = 6;
4
+ export class ContextManager {
5
+ contextWindow;
6
+ messages = [];
7
+ compactedBlock = null;
8
+ iterationsSinceCompaction = 0;
9
+ budget;
10
+ compactionThreshold;
11
+ fileFacts = [];
12
+ decisionFacts = [];
13
+ errorFacts = [];
14
+ tokenCounter;
15
+ pendingImageParts = [];
16
+ onCompact = null;
17
+ constructor(contextWindow, contextBudget, tokenCounter) {
18
+ this.contextWindow = contextWindow;
19
+ this.compactionThreshold = contextBudget?.compactionThreshold ?? 0.75;
20
+ this.budget = this.calculateBudget(contextWindow, contextBudget);
21
+ this.tokenCounter = tokenCounter ?? null;
22
+ }
23
+ calculateBudget(window, contextBudget) {
24
+ if (contextBudget) {
25
+ const systemPrompt = Math.floor(window * contextBudget.systemPrompt);
26
+ const responseReserve = Math.floor(window * contextBudget.responseReserve);
27
+ const history = Math.max(0, window - systemPrompt - responseReserve);
28
+ return { systemPrompt, responseReserve, history };
29
+ }
30
+ // Fallback for backwards compatibility (tests, subagents without explicit budget)
31
+ return {
32
+ systemPrompt: Math.floor(window * 0.25),
33
+ responseReserve: Math.floor(window * 0.12),
34
+ history: Math.floor(window * 0.63),
35
+ };
36
+ }
37
+ getBudget() {
38
+ return { ...this.budget };
39
+ }
40
+ addMessage(msg) {
41
+ // Auto-attach pending images to the next user message
42
+ if (msg.role === "user" && this.pendingImageParts.length > 0) {
43
+ const textPart = {
44
+ type: "text",
45
+ text: typeof msg.content === "string" ? msg.content : getMessageText(msg.content),
46
+ };
47
+ msg = {
48
+ ...msg,
49
+ content: [textPart, ...this.pendingImageParts],
50
+ };
51
+ this.pendingImageParts = [];
52
+ }
53
+ this.messages.push(msg);
54
+ this.iterationsSinceCompaction++;
55
+ }
56
+ /**
57
+ * Queue an image part to be attached to the next user message.
58
+ */
59
+ addPendingImage(part) {
60
+ this.pendingImageParts.push(part);
61
+ }
62
+ /**
63
+ * Check if there are pending images waiting to be attached.
64
+ */
65
+ hasPendingImages() {
66
+ return this.pendingImageParts.length > 0;
67
+ }
68
+ /**
69
+ * Get pending image parts without clearing them.
70
+ */
71
+ getPendingImages() {
72
+ return [...this.pendingImageParts];
73
+ }
74
+ /**
75
+ * Clear pending images (e.g., if user sends a text-only message).
76
+ */
77
+ clearPendingImages() {
78
+ this.pendingImageParts = [];
79
+ }
80
+ getMessageCount() {
81
+ return this.messages.length;
82
+ }
83
+ estimateMessageTokens(m) {
84
+ const text = getMessageText(m.content);
85
+ if (this.tokenCounter) {
86
+ let t = this.tokenCounter.count(text);
87
+ // Image tokens: base64 ~130 tokens per 512x512 tile; rough estimate
88
+ if (Array.isArray(m.content)) {
89
+ for (const part of m.content) {
90
+ if (part.type === "image_url" && part.image_url?.url) {
91
+ const b64Len = part.image_url.url.includes(",")
92
+ ? part.image_url.url.split(",")[1]?.length ?? 0
93
+ : part.image_url.url.length;
94
+ // ~130 tokens per 512 bytes of base64
95
+ t += Math.ceil(b64Len / 512) * 130;
96
+ }
97
+ }
98
+ }
99
+ if (m.tool_calls) {
100
+ for (const tc of m.tool_calls) {
101
+ t += this.tokenCounter.count(tc.id);
102
+ t += this.tokenCounter.count(tc.function.name);
103
+ t += this.tokenCounter.count(tc.function.arguments);
104
+ t += 4;
105
+ }
106
+ }
107
+ return t;
108
+ }
109
+ let t = Math.ceil(text.length / 2);
110
+ if (Array.isArray(m.content)) {
111
+ for (const part of m.content) {
112
+ if (part.type === "image_url" && part.image_url?.url) {
113
+ const b64Len = part.image_url.url.includes(",")
114
+ ? part.image_url.url.split(",")[1]?.length ?? 0
115
+ : part.image_url.url.length;
116
+ t += Math.ceil(b64Len / 512) * 130;
117
+ }
118
+ }
119
+ }
120
+ if (m.tool_calls) {
121
+ for (const tc of m.tool_calls) {
122
+ t += Math.ceil(tc.id.length / 2);
123
+ t += Math.ceil(tc.function.name.length / 2);
124
+ t += Math.ceil(tc.function.arguments.length / 2);
125
+ t += 4;
126
+ }
127
+ }
128
+ return t;
129
+ }
130
+ needsCompaction() {
131
+ if (this.iterationsSinceCompaction >= COMPACTION_INTERVAL)
132
+ return true;
133
+ const totalTokens = this.messages.reduce((sum, m) => sum + this.estimateMessageTokens(m), 0);
134
+ return totalTokens > this.budget.history * this.compactionThreshold;
135
+ }
136
+ compact() {
137
+ if (this.messages.length <= KEEP_LAST_N * 2)
138
+ return;
139
+ const cutoff = this.messages.length - KEEP_LAST_N * 2;
140
+ const oldTurns = this.messages.slice(0, cutoff);
141
+ const recentTurns = this.messages.slice(cutoff);
142
+ this.extractFacts(oldTurns);
143
+ const parts = [];
144
+ parts.push(`[Compressed: ${oldTurns.length} old turns removed]`);
145
+ if (this.fileFacts.length > 0) {
146
+ parts.push(`[Files: ${this.fileFacts.slice(-15).join("; ")}]`);
147
+ }
148
+ if (this.decisionFacts.length > 0) {
149
+ parts.push(`[Decisions: ${this.decisionFacts.slice(-5).join("; ")}]`);
150
+ }
151
+ if (this.errorFacts.length > 0) {
152
+ parts.push(`[Errors: ${this.errorFacts.slice(-3).join("; ")}]`);
153
+ }
154
+ this.compactedBlock = parts.join(" ");
155
+ const summary = {
156
+ role: "user",
157
+ content: `<system-summary>${this.compactedBlock}</system-summary>`,
158
+ };
159
+ const firstSystem = this.messages.find((m) => m.role === "system");
160
+ this.messages = [
161
+ ...(firstSystem ? [firstSystem] : []),
162
+ summary,
163
+ ...recentTurns,
164
+ ];
165
+ this.iterationsSinceCompaction = 0;
166
+ if (this.onCompact) {
167
+ this.onCompact(summary);
168
+ }
169
+ }
170
+ extractFacts(turns) {
171
+ const filePatterns = [
172
+ /(?:Created|Updated|Written|Deleted|Moved) ([\w./\\-]+\.[a-z]+)/gi,
173
+ /Файл (?:создан|обновлён|записан|удалён|перемещён):? ([\w./\\-]+\.[a-z]+)/gi,
174
+ /file (?:created|updated|written|deleted|moved):? ([\w./\\-]+\.[a-z]+)/gi,
175
+ ];
176
+ for (const msg of turns) {
177
+ const content = getMessageText(msg.content);
178
+ if (msg.role === "tool") {
179
+ for (const pattern of filePatterns) {
180
+ for (const match of content.matchAll(pattern)) {
181
+ this.fileFacts.push(match[1]);
182
+ }
183
+ }
184
+ if (content.includes("Plan:") && content.includes("[")) {
185
+ const planLine = content
186
+ .split("\n")
187
+ .find((line) => line.includes("Plan:"));
188
+ if (planLine)
189
+ this.decisionFacts.push(planLine.trim());
190
+ }
191
+ }
192
+ if (msg.role === "assistant") {
193
+ if (content.includes("decided:") || content.includes("decision:")) {
194
+ const line = content
195
+ .split("\n")
196
+ .find((l) => l.includes("decided:") || l.includes("decision:"));
197
+ if (line)
198
+ this.decisionFacts.push(line.trim().slice(0, 200));
199
+ }
200
+ }
201
+ if (content.includes("Error:") ||
202
+ content.includes("failed") ||
203
+ content.includes("Ошибка:") ||
204
+ content.includes("не удалось")) {
205
+ const line = content
206
+ .split("\n")
207
+ .find((l) => l.includes("Error:") ||
208
+ l.includes("failed") ||
209
+ l.includes("Ошибка:") ||
210
+ l.includes("не удалось"));
211
+ if (line)
212
+ this.errorFacts.push(line.trim().slice(0, 250));
213
+ }
214
+ }
215
+ }
216
+ /**
217
+ * Replace the system prompt in place (keeps it first) or prepend a new one.
218
+ * Used to refresh dynamic prompt blocks (e.g. the plan checklist) mid-run.
219
+ */
220
+ updateSystemPrompt(content) {
221
+ const idx = this.messages.findIndex((m) => m.role === "system");
222
+ if (idx >= 0) {
223
+ this.messages[idx] = { ...this.messages[idx], content };
224
+ }
225
+ else {
226
+ this.messages.unshift({ role: "system", content });
227
+ }
228
+ }
229
+ getActiveHistory() {
230
+ return [...this.messages];
231
+ }
232
+ clear() {
233
+ this.messages = [];
234
+ this.compactedBlock = null;
235
+ this.iterationsSinceCompaction = 0;
236
+ }
237
+ getEstimatedTokens() {
238
+ return this.messages.reduce((sum, m) => sum + this.estimateMessageTokens(m), 0);
239
+ }
240
+ }
@@ -0,0 +1,72 @@
1
+ import { execSync } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { resolve, join } from 'path';
4
+ import { t } from '../../i18n/index';
5
+ export class Auditor {
6
+ baseDir;
7
+ constructor(baseDir) {
8
+ this.baseDir = baseDir;
9
+ }
10
+ async audit(plan) {
11
+ const allStepText = plan.steps.map(s => s.description).join(' ');
12
+ const fileMatches = allStepText.match(/\b[\w./-]+\.[a-z]+/gi) || [];
13
+ const uniqueFiles = [...new Set(fileMatches)];
14
+ const missingFiles = [];
15
+ const existingFiles = [];
16
+ for (const filePath of uniqueFiles) {
17
+ const resolved = resolve(this.baseDir, filePath);
18
+ if (existsSync(resolved)) {
19
+ existingFiles.push(filePath);
20
+ }
21
+ else {
22
+ missingFiles.push(filePath);
23
+ }
24
+ }
25
+ const doneSteps = plan.steps.filter(s => s.status === 'done').length;
26
+ const totalSteps = plan.steps.length;
27
+ const typeCheckError = await this.runProjectTypeCheck();
28
+ const passed = missingFiles.length === 0 && !typeCheckError;
29
+ let summary;
30
+ if (passed) {
31
+ summary = t('exec.audit_pass', { done: doneSteps, total: totalSteps, files: existingFiles.length });
32
+ }
33
+ else {
34
+ const missingCount = missingFiles.length;
35
+ if (typeCheckError) {
36
+ summary = t('exec.audit_fail_typecheck', { done: doneSteps, total: totalSteps, missing: missingCount, typeError: typeCheckError });
37
+ }
38
+ else {
39
+ summary = t('exec.audit_fail', { done: doneSteps, total: totalSteps, files: missingCount });
40
+ }
41
+ }
42
+ return {
43
+ passed,
44
+ missingFiles,
45
+ createdFiles: existingFiles,
46
+ modifiedFiles: [],
47
+ summary,
48
+ typeCheckError,
49
+ };
50
+ }
51
+ async runProjectTypeCheck() {
52
+ const tsconfigPath = join(this.baseDir, 'tsconfig.json');
53
+ if (!existsSync(tsconfigPath)) {
54
+ return null;
55
+ }
56
+ try {
57
+ execSync(`npx tsc --noEmit --skipLibCheck`, { cwd: this.baseDir, stdio: 'pipe', timeout: 30000 });
58
+ return null;
59
+ }
60
+ catch (err) {
61
+ if (err.status === 127 || err.message.includes('not found') || err.message.includes('ENOENT')) {
62
+ return null;
63
+ }
64
+ const stderr = err.stderr?.toString() || err.stdout?.toString() || '';
65
+ if (stderr.includes('error TS')) {
66
+ const firstError = stderr.split('\n').find((line) => line.includes('error TS')) || 'TypeScript type error';
67
+ return firstError.trim();
68
+ }
69
+ return null;
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,6 @@
1
+ export { ExecutionModule } from './module';
2
+ export { PlanCreator } from './planner';
3
+ export { PlanTracker } from './tracker';
4
+ export { StepVerifier } from './verifier';
5
+ export { StuckDetector } from './stuck-detector';
6
+ export { Auditor } from './auditor';
@@ -0,0 +1,337 @@
1
+ import { t } from "../../i18n/index";
2
+ import { PlanCreator } from "./planner";
3
+ import { PlanTracker } from "./tracker";
4
+ import { StepVerifier } from "./verifier";
5
+ import { StuckDetector } from "./stuck-detector";
6
+ import { Auditor } from "./auditor";
7
+ import { existsSync } from "fs";
8
+ import { resolve } from "path";
9
+ const STUCK_RECOVERY_COOLDOWN = 5;
10
+ export class ExecutionModule {
11
+ name = "execution";
12
+ tracker = null;
13
+ verifier;
14
+ stuckDetector;
15
+ auditor;
16
+ baseDir;
17
+ lastRecoveryIteration = -STUCK_RECOVERY_COOLDOWN;
18
+ constructor(baseDir, stuckThreshold = 8) {
19
+ this.baseDir = baseDir;
20
+ this.verifier = new StepVerifier(baseDir);
21
+ this.stuckDetector = new StuckDetector(stuckThreshold);
22
+ this.auditor = new Auditor(baseDir);
23
+ }
24
+ setPlan(plan) {
25
+ this.tracker = new PlanTracker(plan);
26
+ }
27
+ getTracker() {
28
+ return this.tracker;
29
+ }
30
+ getStuckDetector() {
31
+ return this.stuckDetector;
32
+ }
33
+ /**
34
+ * Final audit before the agent may declare the task done. Returns null when
35
+ * no plan is active; otherwise checks plan completion + artifact existence.
36
+ */
37
+ async runFinalAudit() {
38
+ if (!this.tracker)
39
+ return null;
40
+ const plan = this.tracker.getPlan();
41
+ const audit = await this.auditor.audit(plan);
42
+ const pendingSteps = plan.steps
43
+ .filter((s) => s.status !== "done" && s.status !== "skipped")
44
+ .map((s) => `${s.id}. ${s.description}`);
45
+ const done = plan.steps.filter((s) => s.status === "done").length;
46
+ const passed = audit.passed && pendingSteps.length === 0;
47
+ return {
48
+ passed,
49
+ done,
50
+ total: plan.steps.length,
51
+ pendingSteps,
52
+ missingFiles: audit.missingFiles,
53
+ summary: audit.summary,
54
+ };
55
+ }
56
+ getSystemPromptBlock() {
57
+ if (!this.tracker)
58
+ return null;
59
+ return {
60
+ content: this.tracker.toPromptBlock(),
61
+ priority: "critical",
62
+ essential: true,
63
+ estimatedTokens: 200,
64
+ };
65
+ }
66
+ getToolDefinitions() {
67
+ return [
68
+ {
69
+ name: "plan",
70
+ description: 'Create, update, show, or abort a multi-step plan. Use "create" at the start of complex tasks. Use "update" after completing each step to track progress. Use "show" to re-print the current plan checklist.',
71
+ parameters: {
72
+ type: "object",
73
+ properties: {
74
+ action: {
75
+ type: "string",
76
+ enum: ["create", "update", "show", "abort"],
77
+ },
78
+ title: { type: "string" },
79
+ steps: { type: "array", items: { type: "string" } },
80
+ step: { type: "number" },
81
+ status: { type: "string", enum: ["done", "failed", "skipped"] },
82
+ note: { type: "string" },
83
+ },
84
+ required: ["action"],
85
+ },
86
+ handler: async (_ctx, args) => {
87
+ const action = String(args.action);
88
+ if (action === "create") {
89
+ const title = String(args.title || "Task Plan");
90
+ const steps = Array.isArray(args.steps)
91
+ ? args.steps.map(String)
92
+ : [];
93
+ if (steps.length === 0) {
94
+ return { success: false, output: t("plan.no_steps") };
95
+ }
96
+ const plan = PlanCreator.createPlan(title, steps);
97
+ this.setPlan(plan);
98
+ const display = PlanCreator.toPromptBlock(plan, 0);
99
+ return {
100
+ success: true,
101
+ output: t("plan.created", { title, steps: String(steps.length) }),
102
+ display,
103
+ };
104
+ }
105
+ if (action === "show") {
106
+ if (!this.tracker) {
107
+ return { success: false, output: t("plan.no_active") };
108
+ }
109
+ const display = this.tracker.toPromptBlock();
110
+ return {
111
+ success: true,
112
+ output: `${t("plan.show_header")}\n${display}`,
113
+ display,
114
+ };
115
+ }
116
+ if (action === "update" && args.step && this.tracker) {
117
+ this.tracker.updateStepStatus(Number(args.step), args.status || "done");
118
+ if (args.note)
119
+ this.tracker.addNote(Number(args.step), String(args.note));
120
+ const progress = this.tracker.getProgressString();
121
+ const display = this.tracker.toPromptBlock();
122
+ return {
123
+ success: true,
124
+ output: `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })}\n${progress}`,
125
+ display,
126
+ };
127
+ }
128
+ if (action === "abort") {
129
+ this.tracker = null;
130
+ return { success: true, output: t("plan.aborted") };
131
+ }
132
+ if (!this.tracker) {
133
+ return { success: false, output: t("plan.no_active") };
134
+ }
135
+ return {
136
+ success: false,
137
+ output: t("plan.unknown_action", { action }),
138
+ };
139
+ },
140
+ },
141
+ {
142
+ name: "todo",
143
+ description: "Manage sub-tasks within current plan step. Use to break down complex steps into smaller tasks.",
144
+ parameters: {
145
+ type: "object",
146
+ properties: {
147
+ action: { type: "string", enum: ["add", "done", "list"] },
148
+ items: { type: "array", items: { type: "string" } },
149
+ },
150
+ required: ["action"],
151
+ },
152
+ handler: async (_ctx, args) => {
153
+ if (!this.tracker) {
154
+ return { success: false, output: t("plan.no_active") };
155
+ }
156
+ const action = String(args.action);
157
+ const currentStep = this.tracker.getCurrentStep();
158
+ if (!currentStep) {
159
+ return { success: false, output: t("plan.step_not_found") };
160
+ }
161
+ if (action === "add" && Array.isArray(args.items)) {
162
+ const items = args.items.map(String);
163
+ currentStep.note = currentStep.note
164
+ ? `${currentStep.note}; ${items.join(", ")}`
165
+ : items.join(", ");
166
+ return {
167
+ success: true,
168
+ output: t("todo.added", {
169
+ count: String(items.length),
170
+ items: items.join(", "),
171
+ }),
172
+ };
173
+ }
174
+ if (action === "done") {
175
+ return {
176
+ success: true,
177
+ output: t("todo.marked_done", { count: "1" }),
178
+ };
179
+ }
180
+ if (action === "list") {
181
+ const note = currentStep.note || "(no sub-tasks)";
182
+ return {
183
+ success: true,
184
+ output: `Step ${currentStep.id}: ${currentStep.description}\nSub-tasks: ${note}`,
185
+ };
186
+ }
187
+ return {
188
+ success: false,
189
+ output: t("todo.unknown_action", { action }),
190
+ };
191
+ },
192
+ },
193
+ {
194
+ name: "verify",
195
+ description: "Run verification for the current step. Checks files mentioned in the step description.",
196
+ parameters: {
197
+ type: "object",
198
+ properties: {
199
+ step: { type: "number", description: "Step number to verify" },
200
+ },
201
+ },
202
+ handler: async (_ctx, args) => {
203
+ if (!this.tracker)
204
+ return { success: false, output: t("plan.no_active") };
205
+ const step = args.step
206
+ ? this.tracker.getStep(Number(args.step))
207
+ : this.tracker.getCurrentStep();
208
+ if (!step)
209
+ return { success: false, output: t("plan.step_not_found") };
210
+ const result = await this.verifier.verifyStep(step.description);
211
+ return {
212
+ success: result.passed,
213
+ output: result.passed
214
+ ? t("verify.passed")
215
+ : t("verify.failed", {
216
+ details: result.failed.map((f) => f.message).join("; "),
217
+ }),
218
+ };
219
+ },
220
+ },
221
+ ];
222
+ }
223
+ getPlugin() {
224
+ return {
225
+ name: "execution",
226
+ onBeforeThink: (ctx) => {
227
+ const step = this.tracker?.getCurrentStep();
228
+ if (this.tracker && step) {
229
+ this.stuckDetector.setCurrentStep(step.id, step.description);
230
+ this.stuckDetector.recordIteration(step.id);
231
+ }
232
+ else {
233
+ this.stuckDetector.reset();
234
+ }
235
+ const stuckReason = this.stuckDetector.getStuckReason();
236
+ if (stuckReason) {
237
+ ctx.logger?.warn(stuckReason);
238
+ }
239
+ if (this.stuckDetector.isStuck() ||
240
+ this.stuckDetector.hasRepetitiveToolCalls()) {
241
+ const currentIter = typeof ctx.iteration === "number" ? ctx.iteration : 0;
242
+ if (currentIter - this.lastRecoveryIteration >=
243
+ STUCK_RECOVERY_COOLDOWN) {
244
+ const recovery = this.stuckDetector.getRecoveryMessage();
245
+ if (recovery && ctx.contextManager) {
246
+ ctx.contextManager.addMessage({
247
+ role: "user",
248
+ content: `<system-summary>${recovery}</system-summary>`,
249
+ });
250
+ }
251
+ this.lastRecoveryIteration = currentIter;
252
+ }
253
+ }
254
+ },
255
+ onBeforeTool: (ctx, call) => {
256
+ const warning = this.checkPlanAlignment(call);
257
+ if (warning && ctx.contextManager) {
258
+ ctx.contextManager.addMessage({
259
+ role: "user",
260
+ content: `<system-summary>${warning}</system-summary>`,
261
+ });
262
+ }
263
+ return true;
264
+ },
265
+ onToolCall: (ctx) => {
266
+ const toolName = ctx?.toolName;
267
+ const args = ctx?.args;
268
+ if (toolName && args) {
269
+ this.stuckDetector.recordToolCall(toolName, args);
270
+ }
271
+ },
272
+ onAfterTool: (ctx, call, result) => {
273
+ if (!result.success) {
274
+ this.stuckDetector.recordToolError(call.name);
275
+ }
276
+ else {
277
+ this.stuckDetector.recordToolSuccess();
278
+ }
279
+ if (this.tracker &&
280
+ result.success &&
281
+ (call.name === "write_file" || call.name === "edit_file")) {
282
+ this.advancePlanIfStepComplete();
283
+ }
284
+ },
285
+ };
286
+ }
287
+ checkPlanAlignment(call) {
288
+ if (!this.tracker)
289
+ return null;
290
+ const step = this.tracker.getCurrentStep();
291
+ if (!step)
292
+ return null;
293
+ const allowedAlways = [
294
+ "plan",
295
+ "todo",
296
+ "verify",
297
+ "list_dir",
298
+ "read_file",
299
+ "glob",
300
+ "grep",
301
+ "file_info",
302
+ "load_skill",
303
+ "question",
304
+ "approve",
305
+ ];
306
+ if (allowedAlways.includes(call.name))
307
+ return null;
308
+ const stepPaths = (step.description.match(/\b[\w./\\-]+\.[a-z]+/gi) || []).map((p) => p.toLowerCase());
309
+ if (stepPaths.length === 0)
310
+ return null;
311
+ const argStr = JSON.stringify(call.arguments).toLowerCase();
312
+ const callPaths = (argStr.match(/\b[\w./\\-]+\.[a-z]+/gi) || []).map((p) => p.toLowerCase());
313
+ if (callPaths.length === 0)
314
+ return null;
315
+ const offPath = callPaths.some((p) => !stepPaths.some((s) => p.includes(s) || s.includes(p)));
316
+ if (!offPath)
317
+ return null;
318
+ return t("exec.plan_warning", {
319
+ step: String(step.id),
320
+ description: step.description,
321
+ tool: call.name,
322
+ });
323
+ }
324
+ advancePlanIfStepComplete() {
325
+ const step = this.tracker?.getCurrentStep();
326
+ if (!step)
327
+ return;
328
+ const stepPaths = step.description.match(/\b[\w./\\-]+\.[a-z]+/gi) || [];
329
+ if (stepPaths.length === 0)
330
+ return;
331
+ const allExist = stepPaths.every((p) => existsSync(resolve(this.baseDir, p)));
332
+ if (allExist) {
333
+ this.tracker?.updateStepStatus(step.id, "done");
334
+ this.tracker?.advance();
335
+ }
336
+ }
337
+ }