micro-models-agent 0.28.0 → 0.28.2

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/main.js +826 -650
  2. package/package.json +1 -1
  3. package/dist/cli/commands.js +0 -220
  4. package/dist/cli/completer.js +0 -168
  5. package/dist/cli/index.js +0 -2
  6. package/dist/cli/main.js +0 -113
  7. package/dist/cli/repl.js +0 -987
  8. package/dist/cli/security-commands.js +0 -166
  9. package/dist/cli/setup.js +0 -229
  10. package/dist/config/config.js +0 -186
  11. package/dist/config/defaults.js +0 -91
  12. package/dist/config/experts.js +0 -15
  13. package/dist/config/index.js +0 -3
  14. package/dist/config/security.js +0 -193
  15. package/dist/config/types.js +0 -1
  16. package/dist/core/agent-moe.js +0 -98
  17. package/dist/core/agent.js +0 -461
  18. package/dist/core/bootstrap.js +0 -321
  19. package/dist/core/index.js +0 -2
  20. package/dist/core/prompt-builder.js +0 -55
  21. package/dist/core/session-logger.js +0 -122
  22. package/dist/core/types.js +0 -1
  23. package/dist/i18n/en.json +0 -461
  24. package/dist/i18n/index.js +0 -43
  25. package/dist/i18n/ru.json +0 -461
  26. package/dist/index.js +0 -22
  27. package/dist/llm/image-utils.js +0 -144
  28. package/dist/llm/index.js +0 -4
  29. package/dist/llm/model-loader.js +0 -78
  30. package/dist/llm/openai-compat.js +0 -324
  31. package/dist/llm/orchestrator.js +0 -194
  32. package/dist/llm/provider.js +0 -10
  33. package/dist/llm/response.js +0 -39
  34. package/dist/llm/token-counter.js +0 -39
  35. package/dist/llm/types.js +0 -1
  36. package/dist/logger/app-logger.js +0 -76
  37. package/dist/logger/index.js +0 -1
  38. package/dist/migration/backup.js +0 -45
  39. package/dist/migration/detect.js +0 -50
  40. package/dist/migration/index.js +0 -2
  41. package/dist/modules/browser/actions.js +0 -46
  42. package/dist/modules/browser/cookie-store.js +0 -24
  43. package/dist/modules/browser/index.js +0 -5
  44. package/dist/modules/browser/module.js +0 -28
  45. package/dist/modules/browser/session.js +0 -287
  46. package/dist/modules/browser/snapshot.js +0 -114
  47. package/dist/modules/browser/types.js +0 -9
  48. package/dist/modules/context/history.js +0 -15
  49. package/dist/modules/context/index.js +0 -1
  50. package/dist/modules/context/manager.js +0 -240
  51. package/dist/modules/execution/auditor.js +0 -72
  52. package/dist/modules/execution/index.js +0 -6
  53. package/dist/modules/execution/module.js +0 -337
  54. package/dist/modules/execution/moe-executor.js +0 -209
  55. package/dist/modules/execution/plan-validator.js +0 -153
  56. package/dist/modules/execution/planner.js +0 -35
  57. package/dist/modules/execution/stuck-detector.js +0 -134
  58. package/dist/modules/execution/tracker.js +0 -53
  59. package/dist/modules/execution/types.js +0 -1
  60. package/dist/modules/execution/verifier.js +0 -149
  61. package/dist/modules/hallucination/confidence.js +0 -54
  62. package/dist/modules/hallucination/consistency.js +0 -60
  63. package/dist/modules/hallucination/detector.js +0 -41
  64. package/dist/modules/hallucination/factual.js +0 -170
  65. package/dist/modules/hallucination/index.js +0 -4
  66. package/dist/modules/index.js +0 -5
  67. package/dist/modules/indexer/cache.js +0 -38
  68. package/dist/modules/indexer/index.js +0 -3
  69. package/dist/modules/indexer/module.js +0 -192
  70. package/dist/modules/indexer/walker.js +0 -101
  71. package/dist/modules/mcp/client.js +0 -393
  72. package/dist/modules/mcp/index.js +0 -3
  73. package/dist/modules/mcp/module.js +0 -146
  74. package/dist/modules/mcp/registry.js +0 -15
  75. package/dist/modules/memory/index.js +0 -1
  76. package/dist/modules/memory/module.js +0 -48
  77. package/dist/modules/memory/search.js +0 -40
  78. package/dist/modules/memory/store.js +0 -65
  79. package/dist/modules/pipelines/engine.js +0 -60
  80. package/dist/modules/pipelines/index.js +0 -3
  81. package/dist/modules/pipelines/parser.js +0 -53
  82. package/dist/modules/pipelines/template.js +0 -14
  83. package/dist/modules/plugins/builtin/lint-on-write.js +0 -121
  84. package/dist/modules/plugins/builtin/notify.js +0 -8
  85. package/dist/modules/plugins/index.js +0 -1
  86. package/dist/modules/plugins/loader.js +0 -28
  87. package/dist/modules/plugins/manager.js +0 -161
  88. package/dist/modules/plugins/types.js +0 -1
  89. package/dist/modules/processes/detect.js +0 -34
  90. package/dist/modules/processes/index.js +0 -3
  91. package/dist/modules/processes/registry.js +0 -148
  92. package/dist/modules/processes/runner.js +0 -124
  93. package/dist/modules/registry.js +0 -45
  94. package/dist/modules/security/audit-log.js +0 -116
  95. package/dist/modules/security/audit-notifier.js +0 -292
  96. package/dist/modules/security/command-validator.js +0 -185
  97. package/dist/modules/security/content-scanner.js +0 -52
  98. package/dist/modules/security/data-sanitizer.js +0 -97
  99. package/dist/modules/security/encryption.js +0 -240
  100. package/dist/modules/security/index.js +0 -14
  101. package/dist/modules/security/network-validator.js +0 -79
  102. package/dist/modules/security/path-validator.js +0 -155
  103. package/dist/modules/security/rate-limiter.js +0 -119
  104. package/dist/modules/security/security-policies.js +0 -393
  105. package/dist/modules/security/session-encryption.js +0 -193
  106. package/dist/modules/security/session-isolation.js +0 -95
  107. package/dist/modules/session/index.js +0 -3
  108. package/dist/modules/session/manager.js +0 -167
  109. package/dist/modules/session/module.js +0 -24
  110. package/dist/modules/session/store.js +0 -174
  111. package/dist/modules/session/types.js +0 -1
  112. package/dist/modules/skills/index.js +0 -3
  113. package/dist/modules/skills/loader.js +0 -72
  114. package/dist/modules/skills/matcher.js +0 -27
  115. package/dist/modules/skills/module.js +0 -143
  116. package/dist/modules/types.js +0 -1
  117. package/dist/modules/updater/checker.js +0 -32
  118. package/dist/modules/updater/index.js +0 -1
  119. package/dist/modules/user-profile/compressor.js +0 -16
  120. package/dist/modules/user-profile/index.js +0 -1
  121. package/dist/modules/user-profile/profile.js +0 -68
  122. package/dist/tools/approve.js +0 -32
  123. package/dist/tools/attach-image.js +0 -89
  124. package/dist/tools/bash.js +0 -140
  125. package/dist/tools/browser.js +0 -97
  126. package/dist/tools/create-dir.js +0 -56
  127. package/dist/tools/delete-file.js +0 -63
  128. package/dist/tools/edit-file.js +0 -77
  129. package/dist/tools/executor.js +0 -95
  130. package/dist/tools/file-info.js +0 -45
  131. package/dist/tools/filter-tools.js +0 -10
  132. package/dist/tools/glob-tool.js +0 -26
  133. package/dist/tools/grep-tool.js +0 -64
  134. package/dist/tools/index.js +0 -52
  135. package/dist/tools/list-dir.js +0 -47
  136. package/dist/tools/load-skill.js +0 -48
  137. package/dist/tools/mcp-call.js +0 -68
  138. package/dist/tools/move-file.js +0 -84
  139. package/dist/tools/path-utils.js +0 -51
  140. package/dist/tools/pipeline-run.js +0 -144
  141. package/dist/tools/preview.js +0 -2
  142. package/dist/tools/process-kill.js +0 -29
  143. package/dist/tools/process-list.js +0 -38
  144. package/dist/tools/process-log.js +0 -41
  145. package/dist/tools/question.js +0 -142
  146. package/dist/tools/read-file.js +0 -73
  147. package/dist/tools/recall.js +0 -110
  148. package/dist/tools/registry.js +0 -36
  149. package/dist/tools/remember.js +0 -67
  150. package/dist/tools/scope-check.js +0 -30
  151. package/dist/tools/search-history.js +0 -64
  152. package/dist/tools/subagent.js +0 -142
  153. package/dist/tools/types.js +0 -1
  154. package/dist/tools/user-input.js +0 -123
  155. package/dist/tools/web-browse.js +0 -57
  156. package/dist/tools/web-fetch.js +0 -72
  157. package/dist/tools/web-search.js +0 -59
  158. package/dist/tools/write-file.js +0 -80
  159. package/dist/ui/box.js +0 -81
  160. package/dist/ui/colors.js +0 -4
  161. package/dist/ui/diff.js +0 -185
  162. package/dist/ui/index.js +0 -6
  163. package/dist/ui/md-formatter.js +0 -212
  164. package/dist/ui/output.js +0 -13
  165. package/dist/ui/renderer.js +0 -141
  166. package/dist/ui/spinner.js +0 -70
  167. package/dist/ui/table.js +0 -144
@@ -1,287 +0,0 @@
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
- export class BrowserActionTracker {
7
- history = [];
8
- threshold;
9
- constructor(threshold = 3) {
10
- this.threshold = threshold;
11
- }
12
- record(action, args, url) {
13
- this.history.push({ action, args, url });
14
- if (this.history.length < this.threshold)
15
- return null;
16
- const recent = this.history.slice(-this.threshold);
17
- const allSame = recent.every(h => h.action === recent[0].action &&
18
- h.url === recent[0].url &&
19
- JSON.stringify(h.args) === JSON.stringify(recent[0].args));
20
- if (allSame) {
21
- return t('browser.repeated_action', { action, threshold: this.threshold });
22
- }
23
- return null;
24
- }
25
- reset() {
26
- this.history = [];
27
- }
28
- }
29
- export class BrowserSession {
30
- browser = null;
31
- context = null;
32
- page = null;
33
- config;
34
- cookieStore;
35
- state = { isOpen: false, url: null, title: null, elementCount: 0 };
36
- actionTracker = new BrowserActionTracker(3);
37
- constructor(config) {
38
- this.config = config;
39
- this.cookieStore = new CookieStore(config.cookieDir);
40
- }
41
- getState() {
42
- return { ...this.state };
43
- }
44
- async execute(action, args) {
45
- try {
46
- let result;
47
- switch (action) {
48
- case 'open':
49
- result = await this.open(String(args.url || ''));
50
- break;
51
- case 'click':
52
- result = await this.click(Number(args.target));
53
- break;
54
- case 'type':
55
- result = await this.type(Number(args.target), String(args.text || ''));
56
- break;
57
- case 'scroll':
58
- result = await this.scroll(String(args.direction || 'down'));
59
- break;
60
- case 'back':
61
- result = await this.back();
62
- break;
63
- case 'forward':
64
- result = await this.forward();
65
- break;
66
- case 'screenshot':
67
- result = await this.screenshot();
68
- break;
69
- case 'snapshot':
70
- result = await this.snapshot();
71
- break;
72
- case 'close':
73
- result = await this.close();
74
- break;
75
- case 'wait':
76
- result = await this.wait(Number(args.ms || 1000));
77
- break;
78
- default: return { success: false, output: t('browser.unknown_action', { action }) };
79
- }
80
- if (action !== 'open') {
81
- const warning = this.actionTracker.record(action, args, this.page?.url() || '');
82
- if (warning && result.success) {
83
- result.output = result.output + '\n\n⚠️ ' + warning;
84
- }
85
- }
86
- return result;
87
- }
88
- catch (err) {
89
- return { success: false, output: t('browser.error', { message: err.message }) };
90
- }
91
- }
92
- async ensurePage() {
93
- if (!this.page) {
94
- return { success: false, output: t('browser.no_page') };
95
- }
96
- return null;
97
- }
98
- async launch() {
99
- if (this.browser)
100
- return;
101
- this.browser = await chromium.launch({ headless: this.config.headless });
102
- this.context = await this.browser.newContext({
103
- viewport: { width: this.config.viewportWidth, height: this.config.viewportHeight },
104
- });
105
- const savedCookies = await this.cookieStore.load();
106
- if (savedCookies.length > 0) {
107
- await this.context.addCookies(savedCookies.map(c => ({
108
- ...c,
109
- sameSite: c.sameSite,
110
- })));
111
- }
112
- this.page = await this.context.newPage();
113
- this.page.setDefaultTimeout(this.config.navigationTimeout);
114
- }
115
- async saveCookies() {
116
- if (!this.context)
117
- return;
118
- const cookies = await this.context.cookies();
119
- await this.cookieStore.save(cookies);
120
- }
121
- async takeSnapshot() {
122
- if (!this.page)
123
- return 'No page open.';
124
- await this.page.evaluate(buildIndexInjectionScript());
125
- const html = await this.page.content();
126
- const url = this.page.url();
127
- const title = await this.page.title();
128
- const allElements = extractInteractiveElements(html, this.config.maxElements + 5);
129
- const truncated = allElements.length > this.config.maxElements;
130
- const elements = allElements.slice(0, this.config.maxElements);
131
- this.state = {
132
- isOpen: true,
133
- url,
134
- title,
135
- elementCount: elements.length,
136
- };
137
- return formatSnapshot({ url, title, elements, truncated });
138
- }
139
- async open(url) {
140
- if (!url)
141
- return { success: false, output: t('browser.url_required') };
142
- if (!url.startsWith('http://') && !url.startsWith('https://')) {
143
- url = 'https://' + url;
144
- }
145
- await this.launch();
146
- if (!this.page)
147
- return { success: false, output: t('browser.create_page_failed') };
148
- this.actionTracker.reset();
149
- try {
150
- await this.page.goto(url, { waitUntil: 'domcontentloaded', timeout: this.config.navigationTimeout });
151
- }
152
- catch (err) {
153
- return { success: false, output: t('browser.nav_failed', { message: err.message }) };
154
- }
155
- const snapshot = await this.takeSnapshot();
156
- await this.saveCookies();
157
- return { success: true, output: snapshot };
158
- }
159
- async click(target) {
160
- const err = await this.ensurePage();
161
- if (err)
162
- return err;
163
- if (!this.page)
164
- return { success: false, output: 'No page' };
165
- if (!target || target < 1) {
166
- return { success: false, output: t('browser.invalid_target') };
167
- }
168
- try {
169
- const script = buildClickScript(target);
170
- await this.page.evaluate(script);
171
- await this.page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => { });
172
- await new Promise(r => setTimeout(r, 500));
173
- const snapshot = await this.takeSnapshot();
174
- await this.saveCookies();
175
- return { success: true, output: snapshot };
176
- }
177
- catch (err) {
178
- return { success: false, output: t('browser.click_failed', { message: err.message }) };
179
- }
180
- }
181
- async type(target, text) {
182
- const err = await this.ensurePage();
183
- if (err)
184
- return err;
185
- if (!this.page)
186
- return { success: false, output: 'No page' };
187
- if (!target || target < 1) {
188
- return { success: false, output: t('browser.invalid_target_short') };
189
- }
190
- if (!text) {
191
- return { success: false, output: t('browser.text_required') };
192
- }
193
- try {
194
- const script = buildTypeScript(target, text);
195
- await this.page.evaluate(script);
196
- const snapshot = await this.takeSnapshot();
197
- return { success: true, output: snapshot };
198
- }
199
- catch (err) {
200
- return { success: false, output: t('browser.type_failed', { message: err.message }) };
201
- }
202
- }
203
- async scroll(direction) {
204
- const err = await this.ensurePage();
205
- if (err)
206
- return err;
207
- if (!this.page)
208
- return { success: false, output: 'No page' };
209
- const dir = direction;
210
- if (!['up', 'down', 'top', 'bottom'].includes(dir)) {
211
- return { success: false, output: t('browser.direction_invalid') };
212
- }
213
- try {
214
- await this.page.evaluate(buildScrollScript(dir));
215
- await new Promise(r => setTimeout(r, 300));
216
- const snapshot = await this.takeSnapshot();
217
- return { success: true, output: snapshot };
218
- }
219
- catch (err) {
220
- return { success: false, output: t('browser.scroll_failed', { message: err.message }) };
221
- }
222
- }
223
- async back() {
224
- const err = await this.ensurePage();
225
- if (err)
226
- return err;
227
- if (!this.page)
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));
231
- const snapshot = await this.takeSnapshot();
232
- return { success: true, output: snapshot };
233
- }
234
- async forward() {
235
- const err = await this.ensurePage();
236
- if (err)
237
- return err;
238
- if (!this.page)
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));
242
- const snapshot = await this.takeSnapshot();
243
- return { success: true, output: snapshot };
244
- }
245
- async screenshot() {
246
- const err = await this.ensurePage();
247
- if (err)
248
- return err;
249
- if (!this.page)
250
- return { success: false, output: 'No page' };
251
- const buffer = await this.page.screenshot({ type: 'png', fullPage: false });
252
- const snapshot = await this.takeSnapshot();
253
- return { success: true, output: snapshot, screenshot: buffer };
254
- }
255
- async snapshot() {
256
- const err = await this.ensurePage();
257
- if (err)
258
- return err;
259
- const snap = await this.takeSnapshot();
260
- return { success: true, output: snap };
261
- }
262
- async wait(ms) {
263
- const err = await this.ensurePage();
264
- if (err)
265
- return err;
266
- await new Promise(r => setTimeout(r, Math.min(ms, 10000)));
267
- const snapshot = await this.takeSnapshot();
268
- return { success: true, output: snapshot };
269
- }
270
- async close() {
271
- await this.saveCookies();
272
- if (this.page) {
273
- await this.page.close().catch(() => { });
274
- this.page = null;
275
- }
276
- if (this.context) {
277
- await this.context.close().catch(() => { });
278
- this.context = null;
279
- }
280
- if (this.browser) {
281
- await this.browser.close().catch(() => { });
282
- this.browser = null;
283
- }
284
- this.state = { isOpen: false, url: null, title: null, elementCount: 0 };
285
- return { success: true, output: t('browser.closed') };
286
- }
287
- }
@@ -1,114 +0,0 @@
1
- import { t } from '../../i18n/index';
2
- function inferRole(tag, type) {
3
- const t = tag.toLowerCase();
4
- if (t === 'a')
5
- return 'link';
6
- if (t === 'button')
7
- return 'button';
8
- if (t === 'input') {
9
- const inputType = type?.toLowerCase();
10
- if (inputType === 'submit' || inputType === 'button')
11
- return 'button';
12
- if (inputType === 'checkbox')
13
- return 'checkbox';
14
- if (inputType === 'radio')
15
- return 'radio';
16
- return 'textbox';
17
- }
18
- if (t === 'textarea')
19
- return 'textbox';
20
- if (t === 'select')
21
- return 'combobox';
22
- return 'widget';
23
- }
24
- function getName(tag, attrs, html, matchIndex) {
25
- const fromAttr = (attrs['aria-label'] ||
26
- attrs['placeholder'] ||
27
- attrs['title'] ||
28
- attrs['alt'] ||
29
- attrs['value'] ||
30
- attrs['name'] ||
31
- '');
32
- if (fromAttr)
33
- return fromAttr;
34
- const afterTag = html.slice(matchIndex);
35
- const closeIdx = afterTag.indexOf('>');
36
- if (closeIdx === -1)
37
- return '';
38
- const afterOpen = afterTag.slice(closeIdx + 1);
39
- const endTag = `</${tag}>`;
40
- const endIdx = afterOpen.indexOf(endTag);
41
- if (endIdx === -1)
42
- return '';
43
- const text = afterOpen.slice(0, endIdx).replace(/<[^>]+>/g, '').trim();
44
- return text.slice(0, 50);
45
- }
46
- export function extractInteractiveElements(html, maxElements = 30) {
47
- const elements = [];
48
- const tagRegex = /<(a|button|input|textarea|select|div|span)([^>]*)>/gi;
49
- let match;
50
- while ((match = tagRegex.exec(html)) !== null) {
51
- if (elements.length >= maxElements)
52
- break;
53
- const tag = match[1].toLowerCase();
54
- const attrStr = match[2];
55
- const attrs = {};
56
- const attrRegex = /(\w[\w-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
57
- let attrMatch;
58
- while ((attrMatch = attrRegex.exec(attrStr)) !== null) {
59
- attrs[attrMatch[1].toLowerCase()] = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? '';
60
- }
61
- if (attrs['style']?.includes('display:none') || attrs['style']?.includes('display: none'))
62
- continue;
63
- if (attrs['hidden'] !== undefined)
64
- continue;
65
- if (attrs['aria-hidden'] === 'true')
66
- continue;
67
- if (tag === 'input' && attrs['type'] === 'hidden')
68
- continue;
69
- const role = attrs['role'] || inferRole(tag, attrs['type']);
70
- const isInteractive = [
71
- 'a', 'button', 'input', 'textarea', 'select',
72
- ].includes(tag) || attrs['role'] !== undefined;
73
- if (!isInteractive)
74
- continue;
75
- if (tag === 'a' && !attrs['href'])
76
- continue;
77
- const name = getName(tag, attrs, html, match.index);
78
- elements.push({
79
- index: elements.length + 1,
80
- tag,
81
- role,
82
- name,
83
- value: attrs['value'],
84
- href: attrs['href'],
85
- visible: true,
86
- });
87
- }
88
- return elements;
89
- }
90
- export function formatSnapshot(snapshot) {
91
- const lines = [];
92
- lines.push(`Page: "${snapshot.title}"`);
93
- lines.push(`URL: ${snapshot.url}`);
94
- lines.push('');
95
- if (snapshot.elements.length === 0 && !snapshot.truncated) {
96
- lines.push(t('browser.no_elements'));
97
- return lines.join('\n');
98
- }
99
- for (const el of snapshot.elements) {
100
- let line = `[${el.index}] ${el.role}`;
101
- if (el.name)
102
- line += ` "${el.name}"`;
103
- if (el.href)
104
- line += ` → ${el.href}`;
105
- if (el.value)
106
- line += ` = "${el.value}"`;
107
- lines.push(line);
108
- }
109
- if (snapshot.truncated) {
110
- lines.push('');
111
- lines.push(t('browser.more_elements'));
112
- }
113
- return lines.join('\n');
114
- }
@@ -1,9 +0,0 @@
1
- export const DEFAULT_BROWSER_CONFIG = {
2
- headless: true,
3
- maxElements: 30,
4
- screenshotMaxWidth: 1280,
5
- cookieDir: '',
6
- viewportWidth: 1280,
7
- viewportHeight: 720,
8
- navigationTimeout: 15000,
9
- };
@@ -1,15 +0,0 @@
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 +0,0 @@
1
- export { ContextManager } from './manager';
@@ -1,240 +0,0 @@
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
- }