micro-models-agent 0.19.1 → 0.19.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 +18046 -0
  2. package/package.json +1 -1
  3. package/dist/cli/commands.js +0 -237
  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 -1010
  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 -471
  18. package/dist/core/bootstrap.js +0 -314
  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/index.js +0 -1
  49. package/dist/modules/context/manager.js +0 -240
  50. package/dist/modules/execution/auditor.js +0 -72
  51. package/dist/modules/execution/index.js +0 -6
  52. package/dist/modules/execution/module.js +0 -337
  53. package/dist/modules/execution/moe-executor.js +0 -209
  54. package/dist/modules/execution/plan-validator.js +0 -153
  55. package/dist/modules/execution/planner.js +0 -35
  56. package/dist/modules/execution/stuck-detector.js +0 -134
  57. package/dist/modules/execution/tracker.js +0 -53
  58. package/dist/modules/execution/types.js +0 -1
  59. package/dist/modules/execution/verifier.js +0 -149
  60. package/dist/modules/hallucination/confidence.js +0 -59
  61. package/dist/modules/hallucination/consistency.js +0 -66
  62. package/dist/modules/hallucination/detector.js +0 -33
  63. package/dist/modules/hallucination/index.js +0 -3
  64. package/dist/modules/index.js +0 -5
  65. package/dist/modules/indexer/cache.js +0 -38
  66. package/dist/modules/indexer/index.js +0 -3
  67. package/dist/modules/indexer/module.js +0 -192
  68. package/dist/modules/indexer/walker.js +0 -101
  69. package/dist/modules/mcp/client.js +0 -393
  70. package/dist/modules/mcp/index.js +0 -3
  71. package/dist/modules/mcp/module.js +0 -146
  72. package/dist/modules/mcp/registry.js +0 -15
  73. package/dist/modules/memory/index.js +0 -1
  74. package/dist/modules/memory/module.js +0 -48
  75. package/dist/modules/memory/search.js +0 -40
  76. package/dist/modules/memory/store.js +0 -65
  77. package/dist/modules/pipelines/engine.js +0 -60
  78. package/dist/modules/pipelines/index.js +0 -3
  79. package/dist/modules/pipelines/parser.js +0 -53
  80. package/dist/modules/pipelines/template.js +0 -14
  81. package/dist/modules/plugins/builtin/lint-on-write.js +0 -121
  82. package/dist/modules/plugins/builtin/notify.js +0 -8
  83. package/dist/modules/plugins/index.js +0 -1
  84. package/dist/modules/plugins/loader.js +0 -28
  85. package/dist/modules/plugins/manager.js +0 -161
  86. package/dist/modules/plugins/types.js +0 -1
  87. package/dist/modules/processes/detect.js +0 -34
  88. package/dist/modules/processes/index.js +0 -3
  89. package/dist/modules/processes/registry.js +0 -148
  90. package/dist/modules/processes/runner.js +0 -124
  91. package/dist/modules/registry.js +0 -45
  92. package/dist/modules/security/audit-log.js +0 -116
  93. package/dist/modules/security/audit-notifier.js +0 -292
  94. package/dist/modules/security/command-validator.js +0 -185
  95. package/dist/modules/security/content-scanner.js +0 -52
  96. package/dist/modules/security/data-sanitizer.js +0 -97
  97. package/dist/modules/security/encryption.js +0 -240
  98. package/dist/modules/security/index.js +0 -14
  99. package/dist/modules/security/network-validator.js +0 -79
  100. package/dist/modules/security/path-validator.js +0 -155
  101. package/dist/modules/security/rate-limiter.js +0 -119
  102. package/dist/modules/security/security-policies.js +0 -393
  103. package/dist/modules/security/session-encryption.js +0 -193
  104. package/dist/modules/security/session-isolation.js +0 -95
  105. package/dist/modules/session/index.js +0 -3
  106. package/dist/modules/session/manager.js +0 -167
  107. package/dist/modules/session/module.js +0 -24
  108. package/dist/modules/session/store.js +0 -174
  109. package/dist/modules/session/types.js +0 -1
  110. package/dist/modules/skills/index.js +0 -3
  111. package/dist/modules/skills/loader.js +0 -72
  112. package/dist/modules/skills/matcher.js +0 -27
  113. package/dist/modules/skills/module.js +0 -143
  114. package/dist/modules/types.js +0 -1
  115. package/dist/modules/updater/checker.js +0 -32
  116. package/dist/modules/updater/index.js +0 -1
  117. package/dist/modules/user-profile/compressor.js +0 -16
  118. package/dist/modules/user-profile/index.js +0 -1
  119. package/dist/modules/user-profile/profile.js +0 -68
  120. package/dist/skills/builtin/builtin/git.md +0 -36
  121. package/dist/skills/builtin/builtin/typescript.md +0 -35
  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 -55
  127. package/dist/tools/delete-file.js +0 -62
  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 -70
  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 -79
  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 +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
- }