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.
Files changed (94) hide show
  1. package/dist/cli/commands.js +3 -116
  2. package/dist/cli/main.js +8 -35
  3. package/dist/cli/repl.js +611 -110
  4. package/dist/cli/setup.js +12 -32
  5. package/dist/config/config.js +30 -46
  6. package/dist/config/defaults.js +1 -10
  7. package/dist/config/security.js +8 -15
  8. package/dist/core/agent-moe.js +12 -24
  9. package/dist/core/agent.js +47 -281
  10. package/dist/core/bootstrap.js +36 -52
  11. package/dist/core/session-logger.js +2 -35
  12. package/dist/i18n/en.json +15 -79
  13. package/dist/i18n/index.js +9 -12
  14. package/dist/i18n/ru.json +15 -79
  15. package/dist/index.js +13 -13
  16. package/dist/llm/openai-compat.js +10 -39
  17. package/dist/logger/app-logger.js +16 -83
  18. package/dist/main.js +624 -243
  19. package/dist/modules/browser/session.js +60 -108
  20. package/dist/modules/context/history.js +15 -0
  21. package/dist/modules/context/manager.js +10 -119
  22. package/dist/modules/execution/auditor.js +39 -33
  23. package/dist/modules/execution/index.js +6 -8
  24. package/dist/modules/execution/module.js +32 -474
  25. package/dist/modules/execution/moe-executor.js +40 -97
  26. package/dist/modules/execution/planner.js +13 -63
  27. package/dist/modules/execution/stuck-detector.js +39 -252
  28. package/dist/modules/execution/tracker.js +7 -21
  29. package/dist/modules/execution/verifier.js +17 -46
  30. package/dist/modules/hallucination/confidence.js +2 -7
  31. package/dist/modules/hallucination/consistency.js +42 -8
  32. package/dist/modules/hallucination/detector.js +21 -26
  33. package/dist/modules/hallucination/factual.js +150 -170
  34. package/dist/modules/hallucination/index.js +4 -5
  35. package/dist/modules/index.js +5 -5
  36. package/dist/modules/mcp/client.js +2 -8
  37. package/dist/modules/memory/store.js +0 -4
  38. package/dist/modules/plugins/builtin/lint-on-write.js +38 -143
  39. package/dist/modules/processes/detect.js +34 -0
  40. package/dist/modules/processes/index.js +2 -1
  41. package/dist/modules/processes/registry.js +35 -125
  42. package/dist/modules/processes/runner.js +110 -9
  43. package/dist/modules/security/audit-log.js +10 -30
  44. package/dist/modules/security/command-validator.js +16 -42
  45. package/dist/modules/security/content-scanner.js +8 -9
  46. package/dist/modules/security/network-validator.js +2 -2
  47. package/dist/modules/security/path-validator.js +10 -64
  48. package/dist/modules/security/security-policies.js +67 -221
  49. package/dist/modules/security/session-encryption.js +25 -42
  50. package/dist/modules/session/manager.js +10 -15
  51. package/dist/modules/session/store.js +8 -62
  52. package/dist/modules/skills/index.js +3 -2
  53. package/dist/modules/skills/matcher.js +27 -0
  54. package/dist/modules/skills/module.js +23 -10
  55. package/dist/tools/bash.js +90 -287
  56. package/dist/tools/create-dir.js +1 -0
  57. package/dist/tools/delete-file.js +1 -0
  58. package/dist/tools/edit-file.js +8 -10
  59. package/dist/tools/executor.js +7 -57
  60. package/dist/tools/grep-tool.js +29 -51
  61. package/dist/tools/index.js +40 -55
  62. package/dist/tools/load-skill.js +18 -14
  63. package/dist/tools/move-file.js +2 -3
  64. package/dist/tools/pipeline-run.js +1 -1
  65. package/dist/tools/read-file.js +5 -15
  66. package/dist/tools/search-history.js +22 -42
  67. package/dist/tools/subagent.js +12 -21
  68. package/dist/tools/web-browse.js +25 -54
  69. package/dist/tools/web-fetch.js +34 -60
  70. package/dist/tools/web-search.js +20 -39
  71. package/dist/tools/write-file.js +10 -13
  72. package/dist/ui/diff.js +16 -9
  73. package/dist/ui/renderer.js +6 -69
  74. package/package.json +1 -1
  75. package/dist/cli/repl-commands.js +0 -633
  76. package/dist/core/workspace.js +0 -76
  77. package/dist/logger/file-log.js +0 -151
  78. package/dist/modules/certification/cli.js +0 -176
  79. package/dist/modules/certification/fact-checker.js +0 -84
  80. package/dist/modules/certification/loader.js +0 -111
  81. package/dist/modules/certification/manifest.js +0 -50
  82. package/dist/modules/certification/runner.js +0 -162
  83. package/dist/modules/certification/scenarios.js +0 -124
  84. package/dist/modules/certification/types.js +0 -1
  85. package/dist/modules/execution/plan-coverage.js +0 -68
  86. package/dist/modules/execution/plan-persister.js +0 -46
  87. package/dist/modules/execution/plan-store.js +0 -159
  88. package/dist/modules/hallucination/js-identifiers.js +0 -72
  89. package/dist/modules/hallucination/llm-judge.js +0 -103
  90. package/dist/modules/lsp/client.js +0 -235
  91. package/dist/modules/lsp/config.js +0 -81
  92. package/dist/modules/lsp/index.js +0 -3
  93. package/dist/modules/lsp/module.js +0 -68
  94. package/dist/modules/lsp/types.js +0 -1
@@ -1,8 +1,8 @@
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";
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((h) => h.action === recent[0].action &&
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("browser.repeated_action", {
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 "open":
57
- result = await this.open(String(args.url || ""));
48
+ case 'open':
49
+ result = await this.open(String(args.url || ''));
58
50
  break;
59
- case "click":
51
+ case 'click':
60
52
  result = await this.click(Number(args.target));
61
53
  break;
62
- case "type":
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 "scroll":
66
- result = await this.scroll(String(args.direction || "down"));
57
+ case 'scroll':
58
+ result = await this.scroll(String(args.direction || 'down'));
67
59
  break;
68
- case "back":
60
+ case 'back':
69
61
  result = await this.back();
70
62
  break;
71
- case "forward":
63
+ case 'forward':
72
64
  result = await this.forward();
73
65
  break;
74
- case "screenshot":
66
+ case 'screenshot':
75
67
  result = await this.screenshot();
76
68
  break;
77
- case "snapshot":
69
+ case 'snapshot':
78
70
  result = await this.snapshot();
79
71
  break;
80
- case "close":
72
+ case 'close':
81
73
  result = await this.close();
82
74
  break;
83
- case "wait":
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 !== "open") {
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 + "\n\n⚠️ " + warning;
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("browser.no_page") };
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((c) => ({
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 "No page open.";
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("browser.url_required") };
160
- if (!url.startsWith("http://") &&
161
- !url.startsWith("https://") &&
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("browser.create_page_failed") };
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: "No page" };
164
+ return { success: false, output: 'No page' };
192
165
  if (!target || target < 1) {
193
- return { success: false, output: t("browser.invalid_target") };
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
- .waitForLoadState("domcontentloaded", { timeout: 5000 })
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: "No page" };
186
+ return { success: false, output: 'No page' };
219
187
  if (!target || target < 1) {
220
- return { success: false, output: t("browser.invalid_target_short") };
188
+ return { success: false, output: t('browser.invalid_target_short') };
221
189
  }
222
190
  if (!text) {
223
- return { success: false, output: t("browser.text_required") };
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: "No page" };
208
+ return { success: false, output: 'No page' };
244
209
  const dir = direction;
245
- if (!["up", "down", "top", "bottom"].includes(dir)) {
246
- return { success: false, output: t("browser.direction_invalid") };
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((r) => setTimeout(r, 300));
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: "No page" };
267
- await this.page
268
- .goBack({
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: "No page" };
283
- await this.page
284
- .goForward({
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: "No page" };
299
- const buffer = await this.page.screenshot({ type: "png", fullPage: false });
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((r) => setTimeout(r, Math.min(ms, 10000)));
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("browser.closed") };
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
- const tokens = this.getEstimatedTokens();
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
- ? (part.image_url.url.split(",")[1]?.length ?? 0)
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
- ? (part.image_url.url.split(",")[1]?.length ?? 0)
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
- ...freshRecent,
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
- newFiles.push(match[1]);
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
- newDecisions.push(planLine.trim());
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
- newDecisions.push(line.trim().slice(0, 200));
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
- newErrors.push(line.trim().slice(0, 250));
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 (this.messages.reduce((sum, m) => sum + this.estimateMessageTokens(m), 0) +
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
  }