micro-models-agent 0.28.9 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands.js +220 -0
- package/dist/cli/completer.js +168 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/main.js +113 -0
- package/dist/cli/repl.js +987 -0
- package/dist/cli/security-commands.js +166 -0
- package/dist/cli/setup.js +229 -0
- package/dist/config/config.js +186 -0
- package/dist/config/defaults.js +91 -0
- package/dist/config/experts.js +15 -0
- package/dist/config/index.js +3 -0
- package/dist/config/security.js +193 -0
- package/dist/config/types.js +1 -0
- package/dist/core/agent-moe.js +98 -0
- package/dist/core/agent.js +461 -0
- package/dist/core/bootstrap.js +321 -0
- package/dist/core/index.js +2 -0
- package/dist/core/prompt-builder.js +55 -0
- package/dist/core/session-logger.js +122 -0
- package/dist/core/types.js +1 -0
- package/dist/i18n/en.json +461 -0
- package/dist/i18n/index.js +43 -0
- package/dist/i18n/ru.json +461 -0
- package/dist/index.js +22 -0
- package/dist/llm/image-utils.js +144 -0
- package/dist/llm/index.js +4 -0
- package/dist/llm/model-loader.js +78 -0
- package/dist/llm/openai-compat.js +324 -0
- package/dist/llm/orchestrator.js +194 -0
- package/dist/llm/provider.js +10 -0
- package/dist/llm/response.js +39 -0
- package/dist/llm/token-counter.js +39 -0
- package/dist/llm/types.js +1 -0
- package/dist/logger/app-logger.js +76 -0
- package/dist/logger/index.js +1 -0
- package/dist/main.js +2251 -724
- package/dist/migration/backup.js +45 -0
- package/dist/migration/detect.js +50 -0
- package/dist/migration/index.js +2 -0
- package/dist/modules/browser/actions.js +46 -0
- package/dist/modules/browser/cookie-store.js +24 -0
- package/dist/modules/browser/index.js +5 -0
- package/dist/modules/browser/module.js +28 -0
- package/dist/modules/browser/session.js +287 -0
- package/dist/modules/browser/snapshot.js +114 -0
- package/dist/modules/browser/types.js +9 -0
- package/dist/modules/context/history.js +15 -0
- package/dist/modules/context/index.js +1 -0
- package/dist/modules/context/manager.js +240 -0
- package/dist/modules/execution/auditor.js +72 -0
- package/dist/modules/execution/index.js +6 -0
- package/dist/modules/execution/module.js +337 -0
- package/dist/modules/execution/moe-executor.js +209 -0
- package/dist/modules/execution/plan-validator.js +153 -0
- package/dist/modules/execution/planner.js +35 -0
- package/dist/modules/execution/stuck-detector.js +134 -0
- package/dist/modules/execution/tracker.js +53 -0
- package/dist/modules/execution/types.js +1 -0
- package/dist/modules/execution/verifier.js +149 -0
- package/dist/modules/hallucination/confidence.js +54 -0
- package/dist/modules/hallucination/consistency.js +60 -0
- package/dist/modules/hallucination/detector.js +41 -0
- package/dist/modules/hallucination/factual.js +170 -0
- package/dist/modules/hallucination/index.js +4 -0
- package/dist/modules/index.js +5 -0
- package/dist/modules/indexer/cache.js +38 -0
- package/dist/modules/indexer/index.js +3 -0
- package/dist/modules/indexer/module.js +192 -0
- package/dist/modules/indexer/walker.js +101 -0
- package/dist/modules/mcp/client.js +393 -0
- package/dist/modules/mcp/index.js +3 -0
- package/dist/modules/mcp/module.js +146 -0
- package/dist/modules/mcp/registry.js +15 -0
- package/dist/modules/memory/index.js +1 -0
- package/dist/modules/memory/module.js +48 -0
- package/dist/modules/memory/search.js +40 -0
- package/dist/modules/memory/store.js +65 -0
- package/dist/modules/pipelines/engine.js +60 -0
- package/dist/modules/pipelines/index.js +3 -0
- package/dist/modules/pipelines/parser.js +53 -0
- package/dist/modules/pipelines/template.js +14 -0
- package/dist/modules/plugins/builtin/lint-on-write.js +121 -0
- package/dist/modules/plugins/builtin/notify.js +8 -0
- package/dist/modules/plugins/index.js +1 -0
- package/dist/modules/plugins/loader.js +28 -0
- package/dist/modules/plugins/manager.js +161 -0
- package/dist/modules/plugins/types.js +1 -0
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +3 -0
- package/dist/modules/processes/registry.js +148 -0
- package/dist/modules/processes/runner.js +124 -0
- package/dist/modules/registry.js +45 -0
- package/dist/modules/security/audit-log.js +116 -0
- package/dist/modules/security/audit-notifier.js +292 -0
- package/dist/modules/security/command-validator.js +185 -0
- package/dist/modules/security/content-scanner.js +52 -0
- package/dist/modules/security/data-sanitizer.js +97 -0
- package/dist/modules/security/encryption.js +240 -0
- package/dist/modules/security/index.js +14 -0
- package/dist/modules/security/network-validator.js +79 -0
- package/dist/modules/security/path-validator.js +155 -0
- package/dist/modules/security/rate-limiter.js +119 -0
- package/dist/modules/security/security-policies.js +393 -0
- package/dist/modules/security/session-encryption.js +193 -0
- package/dist/modules/security/session-isolation.js +95 -0
- package/dist/modules/session/index.js +3 -0
- package/dist/modules/session/manager.js +167 -0
- package/dist/modules/session/module.js +24 -0
- package/dist/modules/session/store.js +174 -0
- package/dist/modules/session/types.js +1 -0
- package/dist/modules/skills/index.js +3 -0
- package/dist/modules/skills/loader.js +72 -0
- package/dist/modules/skills/matcher.js +27 -0
- package/dist/modules/skills/module.js +143 -0
- package/dist/modules/types.js +1 -0
- package/dist/modules/updater/checker.js +32 -0
- package/dist/modules/updater/index.js +1 -0
- package/dist/modules/user-profile/compressor.js +16 -0
- package/dist/modules/user-profile/index.js +1 -0
- package/dist/modules/user-profile/profile.js +68 -0
- package/dist/tools/approve.js +32 -0
- package/dist/tools/attach-image.js +89 -0
- package/dist/tools/bash.js +140 -0
- package/dist/tools/browser.js +97 -0
- package/dist/tools/create-dir.js +56 -0
- package/dist/tools/delete-file.js +63 -0
- package/dist/tools/edit-file.js +77 -0
- package/dist/tools/executor.js +95 -0
- package/dist/tools/file-info.js +45 -0
- package/dist/tools/filter-tools.js +10 -0
- package/dist/tools/glob-tool.js +26 -0
- package/dist/tools/grep-tool.js +64 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/list-dir.js +47 -0
- package/dist/tools/load-skill.js +48 -0
- package/dist/tools/mcp-call.js +68 -0
- package/dist/tools/move-file.js +84 -0
- package/dist/tools/path-utils.js +51 -0
- package/dist/tools/pipeline-run.js +144 -0
- package/dist/tools/preview.js +2 -0
- package/dist/tools/process-kill.js +29 -0
- package/dist/tools/process-list.js +38 -0
- package/dist/tools/process-log.js +41 -0
- package/dist/tools/question.js +142 -0
- package/dist/tools/read-file.js +73 -0
- package/dist/tools/recall.js +110 -0
- package/dist/tools/registry.js +36 -0
- package/dist/tools/remember.js +67 -0
- package/dist/tools/scope-check.js +30 -0
- package/dist/tools/search-history.js +64 -0
- package/dist/tools/subagent.js +142 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/user-input.js +123 -0
- package/dist/tools/web-browse.js +57 -0
- package/dist/tools/web-fetch.js +72 -0
- package/dist/tools/web-search.js +59 -0
- package/dist/tools/write-file.js +80 -0
- package/dist/ui/box.js +81 -0
- package/dist/ui/colors.js +4 -0
- package/dist/ui/diff.js +185 -0
- package/dist/ui/index.js +6 -0
- package/dist/ui/md-formatter.js +212 -0
- package/dist/ui/output.js +13 -0
- package/dist/ui/renderer.js +141 -0
- package/dist/ui/spinner.js +70 -0
- package/dist/ui/table.js +144 -0
- package/package.json +4 -4
|
@@ -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,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
|
+
}
|