micro-models-agent 0.28.17 → 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 +3 -116
- package/dist/cli/main.js +8 -35
- package/dist/cli/repl.js +611 -110
- package/dist/cli/setup.js +12 -32
- package/dist/config/config.js +30 -46
- package/dist/config/defaults.js +1 -10
- package/dist/config/security.js +8 -15
- package/dist/core/agent-moe.js +12 -24
- package/dist/core/agent.js +47 -281
- package/dist/core/bootstrap.js +36 -52
- package/dist/core/session-logger.js +2 -35
- package/dist/i18n/en.json +15 -79
- package/dist/i18n/index.js +9 -12
- package/dist/i18n/ru.json +15 -79
- package/dist/index.js +13 -13
- package/dist/llm/openai-compat.js +10 -39
- package/dist/logger/app-logger.js +16 -83
- package/dist/main.js +624 -243
- package/dist/modules/browser/session.js +60 -108
- package/dist/modules/context/history.js +15 -0
- package/dist/modules/context/manager.js +10 -119
- package/dist/modules/execution/auditor.js +39 -33
- package/dist/modules/execution/index.js +6 -8
- package/dist/modules/execution/module.js +32 -474
- package/dist/modules/execution/moe-executor.js +40 -97
- package/dist/modules/execution/planner.js +13 -63
- package/dist/modules/execution/stuck-detector.js +39 -252
- package/dist/modules/execution/tracker.js +7 -21
- package/dist/modules/execution/verifier.js +17 -46
- package/dist/modules/hallucination/confidence.js +2 -7
- package/dist/modules/hallucination/consistency.js +42 -8
- package/dist/modules/hallucination/detector.js +21 -26
- package/dist/modules/hallucination/factual.js +150 -170
- package/dist/modules/hallucination/index.js +4 -5
- package/dist/modules/index.js +5 -5
- package/dist/modules/mcp/client.js +2 -8
- package/dist/modules/memory/store.js +0 -4
- package/dist/modules/plugins/builtin/lint-on-write.js +38 -143
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +2 -1
- package/dist/modules/processes/registry.js +35 -125
- package/dist/modules/processes/runner.js +110 -9
- package/dist/modules/security/audit-log.js +10 -30
- package/dist/modules/security/command-validator.js +16 -42
- package/dist/modules/security/content-scanner.js +8 -9
- package/dist/modules/security/network-validator.js +2 -2
- package/dist/modules/security/path-validator.js +10 -64
- package/dist/modules/security/security-policies.js +67 -221
- package/dist/modules/security/session-encryption.js +25 -42
- package/dist/modules/session/manager.js +10 -15
- package/dist/modules/session/store.js +8 -62
- package/dist/modules/skills/index.js +3 -2
- package/dist/modules/skills/matcher.js +27 -0
- package/dist/modules/skills/module.js +23 -10
- package/dist/tools/bash.js +90 -287
- package/dist/tools/create-dir.js +1 -0
- package/dist/tools/delete-file.js +1 -0
- package/dist/tools/edit-file.js +8 -10
- package/dist/tools/executor.js +7 -57
- package/dist/tools/grep-tool.js +29 -51
- package/dist/tools/index.js +40 -55
- package/dist/tools/load-skill.js +18 -14
- package/dist/tools/move-file.js +2 -3
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/read-file.js +5 -15
- package/dist/tools/search-history.js +22 -42
- package/dist/tools/subagent.js +12 -21
- package/dist/tools/web-browse.js +25 -54
- package/dist/tools/web-fetch.js +34 -60
- package/dist/tools/web-search.js +20 -39
- package/dist/tools/write-file.js +10 -13
- package/dist/ui/diff.js +16 -9
- package/dist/ui/renderer.js +6 -69
- package/package.json +1 -1
- package/dist/cli/repl-commands.js +0 -633
- package/dist/core/workspace.js +0 -76
- package/dist/logger/file-log.js +0 -151
- package/dist/modules/certification/cli.js +0 -176
- package/dist/modules/certification/fact-checker.js +0 -84
- package/dist/modules/certification/loader.js +0 -111
- package/dist/modules/certification/manifest.js +0 -50
- package/dist/modules/certification/runner.js +0 -162
- package/dist/modules/certification/scenarios.js +0 -124
- package/dist/modules/certification/types.js +0 -1
- package/dist/modules/execution/plan-coverage.js +0 -68
- package/dist/modules/execution/plan-persister.js +0 -46
- package/dist/modules/execution/plan-store.js +0 -159
- package/dist/modules/hallucination/js-identifiers.js +0 -72
- package/dist/modules/hallucination/llm-judge.js +0 -103
- package/dist/modules/lsp/client.js +0 -235
- package/dist/modules/lsp/config.js +0 -81
- package/dist/modules/lsp/index.js +0 -3
- package/dist/modules/lsp/module.js +0 -68
- package/dist/modules/lsp/types.js +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { chromium
|
|
2
|
-
import { extractInteractiveElements, formatSnapshot } from
|
|
3
|
-
import { buildClickScript, buildTypeScript, buildScrollScript, buildIndexInjectionScript
|
|
4
|
-
import { CookieStore } from
|
|
5
|
-
import { t } from
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
import { extractInteractiveElements, formatSnapshot } from './snapshot';
|
|
3
|
+
import { buildClickScript, buildTypeScript, buildScrollScript, buildIndexInjectionScript } from './actions';
|
|
4
|
+
import { CookieStore } from './cookie-store';
|
|
5
|
+
import { t } from '../../i18n/index';
|
|
6
6
|
export class BrowserActionTracker {
|
|
7
7
|
history = [];
|
|
8
8
|
threshold;
|
|
@@ -14,14 +14,11 @@ export class BrowserActionTracker {
|
|
|
14
14
|
if (this.history.length < this.threshold)
|
|
15
15
|
return null;
|
|
16
16
|
const recent = this.history.slice(-this.threshold);
|
|
17
|
-
const allSame = recent.every(
|
|
17
|
+
const allSame = recent.every(h => h.action === recent[0].action &&
|
|
18
18
|
h.url === recent[0].url &&
|
|
19
19
|
JSON.stringify(h.args) === JSON.stringify(recent[0].args));
|
|
20
20
|
if (allSame) {
|
|
21
|
-
return t(
|
|
22
|
-
action,
|
|
23
|
-
threshold: this.threshold,
|
|
24
|
-
});
|
|
21
|
+
return t('browser.repeated_action', { action, threshold: this.threshold });
|
|
25
22
|
}
|
|
26
23
|
return null;
|
|
27
24
|
}
|
|
@@ -35,12 +32,7 @@ export class BrowserSession {
|
|
|
35
32
|
page = null;
|
|
36
33
|
config;
|
|
37
34
|
cookieStore;
|
|
38
|
-
state = {
|
|
39
|
-
isOpen: false,
|
|
40
|
-
url: null,
|
|
41
|
-
title: null,
|
|
42
|
-
elementCount: 0,
|
|
43
|
-
};
|
|
35
|
+
state = { isOpen: false, url: null, title: null, elementCount: 0 };
|
|
44
36
|
actionTracker = new BrowserActionTracker(3);
|
|
45
37
|
constructor(config) {
|
|
46
38
|
this.config = config;
|
|
@@ -53,60 +45,53 @@ export class BrowserSession {
|
|
|
53
45
|
try {
|
|
54
46
|
let result;
|
|
55
47
|
switch (action) {
|
|
56
|
-
case
|
|
57
|
-
result = await this.open(String(args.url ||
|
|
48
|
+
case 'open':
|
|
49
|
+
result = await this.open(String(args.url || ''));
|
|
58
50
|
break;
|
|
59
|
-
case
|
|
51
|
+
case 'click':
|
|
60
52
|
result = await this.click(Number(args.target));
|
|
61
53
|
break;
|
|
62
|
-
case
|
|
63
|
-
result = await this.type(Number(args.target), String(args.text ||
|
|
54
|
+
case 'type':
|
|
55
|
+
result = await this.type(Number(args.target), String(args.text || ''));
|
|
64
56
|
break;
|
|
65
|
-
case
|
|
66
|
-
result = await this.scroll(String(args.direction ||
|
|
57
|
+
case 'scroll':
|
|
58
|
+
result = await this.scroll(String(args.direction || 'down'));
|
|
67
59
|
break;
|
|
68
|
-
case
|
|
60
|
+
case 'back':
|
|
69
61
|
result = await this.back();
|
|
70
62
|
break;
|
|
71
|
-
case
|
|
63
|
+
case 'forward':
|
|
72
64
|
result = await this.forward();
|
|
73
65
|
break;
|
|
74
|
-
case
|
|
66
|
+
case 'screenshot':
|
|
75
67
|
result = await this.screenshot();
|
|
76
68
|
break;
|
|
77
|
-
case
|
|
69
|
+
case 'snapshot':
|
|
78
70
|
result = await this.snapshot();
|
|
79
71
|
break;
|
|
80
|
-
case
|
|
72
|
+
case 'close':
|
|
81
73
|
result = await this.close();
|
|
82
74
|
break;
|
|
83
|
-
case
|
|
75
|
+
case 'wait':
|
|
84
76
|
result = await this.wait(Number(args.ms || 1000));
|
|
85
77
|
break;
|
|
86
|
-
default:
|
|
87
|
-
return {
|
|
88
|
-
success: false,
|
|
89
|
-
output: t("browser.unknown_action", { action }),
|
|
90
|
-
};
|
|
78
|
+
default: return { success: false, output: t('browser.unknown_action', { action }) };
|
|
91
79
|
}
|
|
92
|
-
if (action !==
|
|
93
|
-
const warning = this.actionTracker.record(action, args, this.page?.url() ||
|
|
80
|
+
if (action !== 'open') {
|
|
81
|
+
const warning = this.actionTracker.record(action, args, this.page?.url() || '');
|
|
94
82
|
if (warning && result.success) {
|
|
95
|
-
result.output = result.output +
|
|
83
|
+
result.output = result.output + '\n\n⚠️ ' + warning;
|
|
96
84
|
}
|
|
97
85
|
}
|
|
98
86
|
return result;
|
|
99
87
|
}
|
|
100
88
|
catch (err) {
|
|
101
|
-
return {
|
|
102
|
-
success: false,
|
|
103
|
-
output: t("browser.error", { message: err.message }),
|
|
104
|
-
};
|
|
89
|
+
return { success: false, output: t('browser.error', { message: err.message }) };
|
|
105
90
|
}
|
|
106
91
|
}
|
|
107
92
|
async ensurePage() {
|
|
108
93
|
if (!this.page) {
|
|
109
|
-
return { success: false, output: t(
|
|
94
|
+
return { success: false, output: t('browser.no_page') };
|
|
110
95
|
}
|
|
111
96
|
return null;
|
|
112
97
|
}
|
|
@@ -115,14 +100,11 @@ export class BrowserSession {
|
|
|
115
100
|
return;
|
|
116
101
|
this.browser = await chromium.launch({ headless: this.config.headless });
|
|
117
102
|
this.context = await this.browser.newContext({
|
|
118
|
-
viewport: {
|
|
119
|
-
width: this.config.viewportWidth,
|
|
120
|
-
height: this.config.viewportHeight,
|
|
121
|
-
},
|
|
103
|
+
viewport: { width: this.config.viewportWidth, height: this.config.viewportHeight },
|
|
122
104
|
});
|
|
123
105
|
const savedCookies = await this.cookieStore.load();
|
|
124
106
|
if (savedCookies.length > 0) {
|
|
125
|
-
await this.context.addCookies(savedCookies.map(
|
|
107
|
+
await this.context.addCookies(savedCookies.map(c => ({
|
|
126
108
|
...c,
|
|
127
109
|
sameSite: c.sameSite,
|
|
128
110
|
})));
|
|
@@ -138,7 +120,7 @@ export class BrowserSession {
|
|
|
138
120
|
}
|
|
139
121
|
async takeSnapshot() {
|
|
140
122
|
if (!this.page)
|
|
141
|
-
return
|
|
123
|
+
return 'No page open.';
|
|
142
124
|
await this.page.evaluate(buildIndexInjectionScript());
|
|
143
125
|
const html = await this.page.content();
|
|
144
126
|
const url = this.page.url();
|
|
@@ -156,28 +138,19 @@ export class BrowserSession {
|
|
|
156
138
|
}
|
|
157
139
|
async open(url) {
|
|
158
140
|
if (!url)
|
|
159
|
-
return { success: false, output: t(
|
|
160
|
-
if (!url.startsWith(
|
|
161
|
-
|
|
162
|
-
!url.startsWith("file://")) {
|
|
163
|
-
const isLocalhost = /^(localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?$/i.test(url);
|
|
164
|
-
url = isLocalhost ? "http://" + url : "https://" + url;
|
|
141
|
+
return { success: false, output: t('browser.url_required') };
|
|
142
|
+
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
143
|
+
url = 'https://' + url;
|
|
165
144
|
}
|
|
166
145
|
await this.launch();
|
|
167
146
|
if (!this.page)
|
|
168
|
-
return { success: false, output: t(
|
|
147
|
+
return { success: false, output: t('browser.create_page_failed') };
|
|
169
148
|
this.actionTracker.reset();
|
|
170
149
|
try {
|
|
171
|
-
await this.page.goto(url, {
|
|
172
|
-
waitUntil: "domcontentloaded",
|
|
173
|
-
timeout: this.config.navigationTimeout,
|
|
174
|
-
});
|
|
150
|
+
await this.page.goto(url, { waitUntil: 'domcontentloaded', timeout: this.config.navigationTimeout });
|
|
175
151
|
}
|
|
176
152
|
catch (err) {
|
|
177
|
-
return {
|
|
178
|
-
success: false,
|
|
179
|
-
output: t("browser.nav_failed", { message: err.message }),
|
|
180
|
-
};
|
|
153
|
+
return { success: false, output: t('browser.nav_failed', { message: err.message }) };
|
|
181
154
|
}
|
|
182
155
|
const snapshot = await this.takeSnapshot();
|
|
183
156
|
await this.saveCookies();
|
|
@@ -188,26 +161,21 @@ export class BrowserSession {
|
|
|
188
161
|
if (err)
|
|
189
162
|
return err;
|
|
190
163
|
if (!this.page)
|
|
191
|
-
return { success: false, output:
|
|
164
|
+
return { success: false, output: 'No page' };
|
|
192
165
|
if (!target || target < 1) {
|
|
193
|
-
return { success: false, output: t(
|
|
166
|
+
return { success: false, output: t('browser.invalid_target') };
|
|
194
167
|
}
|
|
195
168
|
try {
|
|
196
169
|
const script = buildClickScript(target);
|
|
197
170
|
await this.page.evaluate(script);
|
|
198
|
-
await this.page
|
|
199
|
-
|
|
200
|
-
.catch(() => { });
|
|
201
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
171
|
+
await this.page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => { });
|
|
172
|
+
await new Promise(r => setTimeout(r, 500));
|
|
202
173
|
const snapshot = await this.takeSnapshot();
|
|
203
174
|
await this.saveCookies();
|
|
204
175
|
return { success: true, output: snapshot };
|
|
205
176
|
}
|
|
206
177
|
catch (err) {
|
|
207
|
-
return {
|
|
208
|
-
success: false,
|
|
209
|
-
output: t("browser.click_failed", { message: err.message }),
|
|
210
|
-
};
|
|
178
|
+
return { success: false, output: t('browser.click_failed', { message: err.message }) };
|
|
211
179
|
}
|
|
212
180
|
}
|
|
213
181
|
async type(target, text) {
|
|
@@ -215,12 +183,12 @@ export class BrowserSession {
|
|
|
215
183
|
if (err)
|
|
216
184
|
return err;
|
|
217
185
|
if (!this.page)
|
|
218
|
-
return { success: false, output:
|
|
186
|
+
return { success: false, output: 'No page' };
|
|
219
187
|
if (!target || target < 1) {
|
|
220
|
-
return { success: false, output: t(
|
|
188
|
+
return { success: false, output: t('browser.invalid_target_short') };
|
|
221
189
|
}
|
|
222
190
|
if (!text) {
|
|
223
|
-
return { success: false, output: t(
|
|
191
|
+
return { success: false, output: t('browser.text_required') };
|
|
224
192
|
}
|
|
225
193
|
try {
|
|
226
194
|
const script = buildTypeScript(target, text);
|
|
@@ -229,10 +197,7 @@ export class BrowserSession {
|
|
|
229
197
|
return { success: true, output: snapshot };
|
|
230
198
|
}
|
|
231
199
|
catch (err) {
|
|
232
|
-
return {
|
|
233
|
-
success: false,
|
|
234
|
-
output: t("browser.type_failed", { message: err.message }),
|
|
235
|
-
};
|
|
200
|
+
return { success: false, output: t('browser.type_failed', { message: err.message }) };
|
|
236
201
|
}
|
|
237
202
|
}
|
|
238
203
|
async scroll(direction) {
|
|
@@ -240,22 +205,19 @@ export class BrowserSession {
|
|
|
240
205
|
if (err)
|
|
241
206
|
return err;
|
|
242
207
|
if (!this.page)
|
|
243
|
-
return { success: false, output:
|
|
208
|
+
return { success: false, output: 'No page' };
|
|
244
209
|
const dir = direction;
|
|
245
|
-
if (![
|
|
246
|
-
return { success: false, output: t(
|
|
210
|
+
if (!['up', 'down', 'top', 'bottom'].includes(dir)) {
|
|
211
|
+
return { success: false, output: t('browser.direction_invalid') };
|
|
247
212
|
}
|
|
248
213
|
try {
|
|
249
214
|
await this.page.evaluate(buildScrollScript(dir));
|
|
250
|
-
await new Promise(
|
|
215
|
+
await new Promise(r => setTimeout(r, 300));
|
|
251
216
|
const snapshot = await this.takeSnapshot();
|
|
252
217
|
return { success: true, output: snapshot };
|
|
253
218
|
}
|
|
254
219
|
catch (err) {
|
|
255
|
-
return {
|
|
256
|
-
success: false,
|
|
257
|
-
output: t("browser.scroll_failed", { message: err.message }),
|
|
258
|
-
};
|
|
220
|
+
return { success: false, output: t('browser.scroll_failed', { message: err.message }) };
|
|
259
221
|
}
|
|
260
222
|
}
|
|
261
223
|
async back() {
|
|
@@ -263,14 +225,9 @@ export class BrowserSession {
|
|
|
263
225
|
if (err)
|
|
264
226
|
return err;
|
|
265
227
|
if (!this.page)
|
|
266
|
-
return { success: false, output:
|
|
267
|
-
await this.page
|
|
268
|
-
|
|
269
|
-
waitUntil: "domcontentloaded",
|
|
270
|
-
timeout: this.config.navigationTimeout,
|
|
271
|
-
})
|
|
272
|
-
.catch(() => { });
|
|
273
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
228
|
+
return { success: false, output: 'No page' };
|
|
229
|
+
await this.page.goBack({ waitUntil: 'domcontentloaded', timeout: this.config.navigationTimeout }).catch(() => { });
|
|
230
|
+
await new Promise(r => setTimeout(r, 300));
|
|
274
231
|
const snapshot = await this.takeSnapshot();
|
|
275
232
|
return { success: true, output: snapshot };
|
|
276
233
|
}
|
|
@@ -279,14 +236,9 @@ export class BrowserSession {
|
|
|
279
236
|
if (err)
|
|
280
237
|
return err;
|
|
281
238
|
if (!this.page)
|
|
282
|
-
return { success: false, output:
|
|
283
|
-
await this.page
|
|
284
|
-
|
|
285
|
-
waitUntil: "domcontentloaded",
|
|
286
|
-
timeout: this.config.navigationTimeout,
|
|
287
|
-
})
|
|
288
|
-
.catch(() => { });
|
|
289
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
239
|
+
return { success: false, output: 'No page' };
|
|
240
|
+
await this.page.goForward({ waitUntil: 'domcontentloaded', timeout: this.config.navigationTimeout }).catch(() => { });
|
|
241
|
+
await new Promise(r => setTimeout(r, 300));
|
|
290
242
|
const snapshot = await this.takeSnapshot();
|
|
291
243
|
return { success: true, output: snapshot };
|
|
292
244
|
}
|
|
@@ -295,8 +247,8 @@ export class BrowserSession {
|
|
|
295
247
|
if (err)
|
|
296
248
|
return err;
|
|
297
249
|
if (!this.page)
|
|
298
|
-
return { success: false, output:
|
|
299
|
-
const buffer = await this.page.screenshot({ type:
|
|
250
|
+
return { success: false, output: 'No page' };
|
|
251
|
+
const buffer = await this.page.screenshot({ type: 'png', fullPage: false });
|
|
300
252
|
const snapshot = await this.takeSnapshot();
|
|
301
253
|
return { success: true, output: snapshot, screenshot: buffer };
|
|
302
254
|
}
|
|
@@ -311,7 +263,7 @@ export class BrowserSession {
|
|
|
311
263
|
const err = await this.ensurePage();
|
|
312
264
|
if (err)
|
|
313
265
|
return err;
|
|
314
|
-
await new Promise(
|
|
266
|
+
await new Promise(r => setTimeout(r, Math.min(ms, 10000)));
|
|
315
267
|
const snapshot = await this.takeSnapshot();
|
|
316
268
|
return { success: true, output: snapshot };
|
|
317
269
|
}
|
|
@@ -330,6 +282,6 @@ export class BrowserSession {
|
|
|
330
282
|
this.browser = null;
|
|
331
283
|
}
|
|
332
284
|
this.state = { isOpen: false, url: null, title: null, elementCount: 0 };
|
|
333
|
-
return { success: true, output: t(
|
|
285
|
+
return { success: true, output: t('browser.closed') };
|
|
334
286
|
}
|
|
335
287
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
export class SessionHistoryWriter {
|
|
4
|
+
historyPath;
|
|
5
|
+
constructor(sessionsDir, sessionId) {
|
|
6
|
+
const sessionDir = join(sessionsDir, sessionId);
|
|
7
|
+
if (!existsSync(sessionDir)) {
|
|
8
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
this.historyPath = join(sessionDir, 'history.jsonl');
|
|
11
|
+
}
|
|
12
|
+
append(msg) {
|
|
13
|
+
appendFileSync(this.historyPath, JSON.stringify(msg) + '\n', 'utf-8');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,64 +1,18 @@
|
|
|
1
1
|
import { getMessageText } from "../../llm/provider";
|
|
2
2
|
const COMPACTION_INTERVAL = 15;
|
|
3
3
|
const KEEP_LAST_N = 6;
|
|
4
|
-
function summarizeArgs(args) {
|
|
5
|
-
if (!args)
|
|
6
|
-
return "";
|
|
7
|
-
if (typeof args === "string")
|
|
8
|
-
return args.slice(0, 80);
|
|
9
|
-
try {
|
|
10
|
-
const keys = Object.keys(args);
|
|
11
|
-
return keys.slice(0, 3).join(", ");
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
return String(args).slice(0, 80);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
function truncate(s, max) {
|
|
18
|
-
if (s.length <= max)
|
|
19
|
-
return s;
|
|
20
|
-
return s.slice(0, max - 3) + "...";
|
|
21
|
-
}
|
|
22
|
-
export function extractTriedAndFailed(messages) {
|
|
23
|
-
const failures = new Map();
|
|
24
|
-
for (const msg of messages) {
|
|
25
|
-
if (msg.role === "tool" && msg.name && msg.success === false) {
|
|
26
|
-
const key = `${msg.name}:${summarizeArgs(msg.arguments)}`;
|
|
27
|
-
const existing = failures.get(key);
|
|
28
|
-
const errorText = truncate(typeof msg.content === "string"
|
|
29
|
-
? msg.content
|
|
30
|
-
: getMessageText(msg.content), 100);
|
|
31
|
-
if (existing) {
|
|
32
|
-
existing.count++;
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
failures.set(key, {
|
|
36
|
-
tool: msg.name,
|
|
37
|
-
args: summarizeArgs(msg.arguments),
|
|
38
|
-
error: errorText,
|
|
39
|
-
count: 1,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return Array.from(failures.values()).filter((f) => f.count >= 2);
|
|
45
|
-
}
|
|
46
4
|
export class ContextManager {
|
|
47
5
|
contextWindow;
|
|
48
6
|
messages = [];
|
|
49
7
|
compactedBlock = null;
|
|
50
8
|
iterationsSinceCompaction = 0;
|
|
51
|
-
compactionCount = 0;
|
|
52
|
-
peakTokens = 0;
|
|
53
9
|
budget;
|
|
54
10
|
compactionThreshold;
|
|
55
11
|
fileFacts = [];
|
|
56
12
|
decisionFacts = [];
|
|
57
13
|
errorFacts = [];
|
|
58
|
-
static MAX_FACTS = 20;
|
|
59
14
|
tokenCounter;
|
|
60
15
|
pendingImageParts = [];
|
|
61
|
-
toolTokens = 0;
|
|
62
16
|
onCompact = null;
|
|
63
17
|
constructor(contextWindow, contextBudget, tokenCounter) {
|
|
64
18
|
this.contextWindow = contextWindow;
|
|
@@ -83,36 +37,12 @@ export class ContextManager {
|
|
|
83
37
|
getBudget() {
|
|
84
38
|
return { ...this.budget };
|
|
85
39
|
}
|
|
86
|
-
getCompactionCount() {
|
|
87
|
-
return this.compactionCount;
|
|
88
|
-
}
|
|
89
|
-
getIterationsSinceCompaction() {
|
|
90
|
-
return this.iterationsSinceCompaction;
|
|
91
|
-
}
|
|
92
|
-
noteIteration() {
|
|
93
|
-
this.iterationsSinceCompaction++;
|
|
94
|
-
}
|
|
95
|
-
getQuality() {
|
|
96
|
-
const usedTokens = this.getEstimatedTokens();
|
|
97
|
-
const tokenLoad = Math.max(0, 1 - usedTokens / this.budget.history);
|
|
98
|
-
const compactionLoss = Math.max(0, 1 - this.compactionCount * 0.15);
|
|
99
|
-
const msgCount = this.messages.length || 1;
|
|
100
|
-
const errorDensity = Math.max(0, 1 - Math.min(1, this.errorFacts.length / msgCount));
|
|
101
|
-
const freshness = Math.max(0, 1 - this.iterationsSinceCompaction / COMPACTION_INTERVAL);
|
|
102
|
-
const score = tokenLoad * 0.4 +
|
|
103
|
-
compactionLoss * 0.25 +
|
|
104
|
-
errorDensity * 0.2 +
|
|
105
|
-
freshness * 0.15;
|
|
106
|
-
return Math.round(Math.min(100, Math.max(0, score * 100)));
|
|
107
|
-
}
|
|
108
40
|
addMessage(msg) {
|
|
109
41
|
// Auto-attach pending images to the next user message
|
|
110
42
|
if (msg.role === "user" && this.pendingImageParts.length > 0) {
|
|
111
43
|
const textPart = {
|
|
112
44
|
type: "text",
|
|
113
|
-
text: typeof msg.content === "string"
|
|
114
|
-
? msg.content
|
|
115
|
-
: getMessageText(msg.content),
|
|
45
|
+
text: typeof msg.content === "string" ? msg.content : getMessageText(msg.content),
|
|
116
46
|
};
|
|
117
47
|
msg = {
|
|
118
48
|
...msg,
|
|
@@ -121,9 +51,7 @@ export class ContextManager {
|
|
|
121
51
|
this.pendingImageParts = [];
|
|
122
52
|
}
|
|
123
53
|
this.messages.push(msg);
|
|
124
|
-
|
|
125
|
-
if (tokens > this.peakTokens)
|
|
126
|
-
this.peakTokens = tokens;
|
|
54
|
+
this.iterationsSinceCompaction++;
|
|
127
55
|
}
|
|
128
56
|
/**
|
|
129
57
|
* Queue an image part to be attached to the next user message.
|
|
@@ -161,7 +89,7 @@ export class ContextManager {
|
|
|
161
89
|
for (const part of m.content) {
|
|
162
90
|
if (part.type === "image_url" && part.image_url?.url) {
|
|
163
91
|
const b64Len = part.image_url.url.includes(",")
|
|
164
|
-
?
|
|
92
|
+
? part.image_url.url.split(",")[1]?.length ?? 0
|
|
165
93
|
: part.image_url.url.length;
|
|
166
94
|
// ~130 tokens per 512 bytes of base64
|
|
167
95
|
t += Math.ceil(b64Len / 512) * 130;
|
|
@@ -183,7 +111,7 @@ export class ContextManager {
|
|
|
183
111
|
for (const part of m.content) {
|
|
184
112
|
if (part.type === "image_url" && part.image_url?.url) {
|
|
185
113
|
const b64Len = part.image_url.url.includes(",")
|
|
186
|
-
?
|
|
114
|
+
? part.image_url.url.split(",")[1]?.length ?? 0
|
|
187
115
|
: part.image_url.url.length;
|
|
188
116
|
t += Math.ceil(b64Len / 512) * 130;
|
|
189
117
|
}
|
|
@@ -206,12 +134,8 @@ export class ContextManager {
|
|
|
206
134
|
return totalTokens > this.budget.history * this.compactionThreshold;
|
|
207
135
|
}
|
|
208
136
|
compact() {
|
|
209
|
-
// Reset the counter even when nothing to compact — otherwise
|
|
210
|
-
// needsCompaction() returns true forever after 15 iterations with few messages.
|
|
211
|
-
this.iterationsSinceCompaction = 0;
|
|
212
137
|
if (this.messages.length <= KEEP_LAST_N * 2)
|
|
213
138
|
return;
|
|
214
|
-
this.compactionCount++;
|
|
215
139
|
const cutoff = this.messages.length - KEEP_LAST_N * 2;
|
|
216
140
|
const oldTurns = this.messages.slice(0, cutoff);
|
|
217
141
|
const recentTurns = this.messages.slice(cutoff);
|
|
@@ -227,28 +151,16 @@ export class ContextManager {
|
|
|
227
151
|
if (this.errorFacts.length > 0) {
|
|
228
152
|
parts.push(`[Errors: ${this.errorFacts.slice(-3).join("; ")}]`);
|
|
229
153
|
}
|
|
230
|
-
const triedAndFailed = extractTriedAndFailed(oldTurns);
|
|
231
|
-
if (triedAndFailed.length > 0) {
|
|
232
|
-
const lines = triedAndFailed.map((t) => `- ${t.tool}(${t.args}): ${t.error} (failed ${t.count}x)`);
|
|
233
|
-
parts.push(`[Already tried & failed — do NOT repeat:]\n${lines.join("\n")}`);
|
|
234
|
-
}
|
|
235
154
|
this.compactedBlock = parts.join(" ");
|
|
236
155
|
const summary = {
|
|
237
156
|
role: "user",
|
|
238
157
|
content: `<system-summary>${this.compactedBlock}</system-summary>`,
|
|
239
158
|
};
|
|
240
159
|
const firstSystem = this.messages.find((m) => m.role === "system");
|
|
241
|
-
// Filter out stale system-summary messages from recent turns to prevent nesting
|
|
242
|
-
const freshRecent = recentTurns.filter((m) => {
|
|
243
|
-
if (m.role !== "user")
|
|
244
|
-
return true;
|
|
245
|
-
const text = getMessageText(m.content);
|
|
246
|
-
return !text.startsWith("<system-summary>");
|
|
247
|
-
});
|
|
248
160
|
this.messages = [
|
|
249
161
|
...(firstSystem ? [firstSystem] : []),
|
|
250
162
|
summary,
|
|
251
|
-
...
|
|
163
|
+
...recentTurns,
|
|
252
164
|
];
|
|
253
165
|
this.iterationsSinceCompaction = 0;
|
|
254
166
|
if (this.onCompact) {
|
|
@@ -261,15 +173,12 @@ export class ContextManager {
|
|
|
261
173
|
/Файл (?:создан|обновлён|записан|удалён|перемещён):? ([\w./\\-]+\.[a-z]+)/gi,
|
|
262
174
|
/file (?:created|updated|written|deleted|moved):? ([\w./\\-]+\.[a-z]+)/gi,
|
|
263
175
|
];
|
|
264
|
-
const newFiles = [];
|
|
265
|
-
const newDecisions = [];
|
|
266
|
-
const newErrors = [];
|
|
267
176
|
for (const msg of turns) {
|
|
268
177
|
const content = getMessageText(msg.content);
|
|
269
178
|
if (msg.role === "tool") {
|
|
270
179
|
for (const pattern of filePatterns) {
|
|
271
180
|
for (const match of content.matchAll(pattern)) {
|
|
272
|
-
|
|
181
|
+
this.fileFacts.push(match[1]);
|
|
273
182
|
}
|
|
274
183
|
}
|
|
275
184
|
if (content.includes("Plan:") && content.includes("[")) {
|
|
@@ -277,7 +186,7 @@ export class ContextManager {
|
|
|
277
186
|
.split("\n")
|
|
278
187
|
.find((line) => line.includes("Plan:"));
|
|
279
188
|
if (planLine)
|
|
280
|
-
|
|
189
|
+
this.decisionFacts.push(planLine.trim());
|
|
281
190
|
}
|
|
282
191
|
}
|
|
283
192
|
if (msg.role === "assistant") {
|
|
@@ -286,7 +195,7 @@ export class ContextManager {
|
|
|
286
195
|
.split("\n")
|
|
287
196
|
.find((l) => l.includes("decided:") || l.includes("decision:"));
|
|
288
197
|
if (line)
|
|
289
|
-
|
|
198
|
+
this.decisionFacts.push(line.trim().slice(0, 200));
|
|
290
199
|
}
|
|
291
200
|
}
|
|
292
201
|
if (content.includes("Error:") ||
|
|
@@ -300,14 +209,9 @@ export class ContextManager {
|
|
|
300
209
|
l.includes("Ошибка:") ||
|
|
301
210
|
l.includes("не удалось"));
|
|
302
211
|
if (line)
|
|
303
|
-
|
|
212
|
+
this.errorFacts.push(line.trim().slice(0, 250));
|
|
304
213
|
}
|
|
305
214
|
}
|
|
306
|
-
// Deduplicate and cap facts to prevent unbounded growth
|
|
307
|
-
const dedup = (arr) => [...new Set(arr)];
|
|
308
|
-
this.fileFacts = dedup([...this.fileFacts, ...newFiles]).slice(-ContextManager.MAX_FACTS);
|
|
309
|
-
this.decisionFacts = dedup([...this.decisionFacts, ...newDecisions]).slice(-ContextManager.MAX_FACTS);
|
|
310
|
-
this.errorFacts = dedup([...this.errorFacts, ...newErrors]).slice(-ContextManager.MAX_FACTS);
|
|
311
215
|
}
|
|
312
216
|
/**
|
|
313
217
|
* Replace the system prompt in place (keeps it first) or prepend a new one.
|
|
@@ -329,21 +233,8 @@ export class ContextManager {
|
|
|
329
233
|
this.messages = [];
|
|
330
234
|
this.compactedBlock = null;
|
|
331
235
|
this.iterationsSinceCompaction = 0;
|
|
332
|
-
this.compactionCount = 0;
|
|
333
|
-
this.peakTokens = 0;
|
|
334
236
|
}
|
|
335
237
|
getEstimatedTokens() {
|
|
336
|
-
return
|
|
337
|
-
this.toolTokens);
|
|
338
|
-
}
|
|
339
|
-
setToolTokens(tokens) {
|
|
340
|
-
this.toolTokens = tokens;
|
|
341
|
-
}
|
|
342
|
-
resize(contextWindow, contextBudget, tokenCounter) {
|
|
343
|
-
this.contextWindow = contextWindow;
|
|
344
|
-
this.budget = this.calculateBudget(contextWindow, contextBudget);
|
|
345
|
-
if (tokenCounter !== undefined) {
|
|
346
|
-
this.tokenCounter = tokenCounter ?? null;
|
|
347
|
-
}
|
|
238
|
+
return this.messages.reduce((sum, m) => sum + this.estimateMessageTokens(m), 0);
|
|
348
239
|
}
|
|
349
240
|
}
|