zerochat 7.3.0__py3-none-any.whl

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 (130) hide show
  1. zerochat-7.3.0.dist-info/METADATA +70 -0
  2. zerochat-7.3.0.dist-info/RECORD +130 -0
  3. zerochat-7.3.0.dist-info/WHEEL +5 -0
  4. zerochat-7.3.0.dist-info/entry_points.txt +2 -0
  5. zerochat-7.3.0.dist-info/licenses/LICENSE +34 -0
  6. zerochat-7.3.0.dist-info/top_level.txt +2 -0
  7. zerochat.py +2004 -0
  8. zerochat_runtime/__init__.py +1 -0
  9. zerochat_runtime/assets/css/base.css +199 -0
  10. zerochat_runtime/assets/css/components/composer.css +1258 -0
  11. zerochat_runtime/assets/css/components/debug.css +777 -0
  12. zerochat_runtime/assets/css/components/header.css +97 -0
  13. zerochat_runtime/assets/css/components/icons.css +39 -0
  14. zerochat_runtime/assets/css/components/markdown.css +685 -0
  15. zerochat_runtime/assets/css/components/messages.css +473 -0
  16. zerochat_runtime/assets/css/components/modals.css +1319 -0
  17. zerochat_runtime/assets/css/components/sidebar.css +622 -0
  18. zerochat_runtime/assets/css/components/tools.css +1223 -0
  19. zerochat_runtime/assets/css/layout.css +171 -0
  20. zerochat_runtime/assets/css/print.css +120 -0
  21. zerochat_runtime/assets/css/styles.css +18 -0
  22. zerochat_runtime/assets/css/theme-overrides.css +555 -0
  23. zerochat_runtime/assets/css/tokens.css +165 -0
  24. zerochat_runtime/assets/help/architecture.html +247 -0
  25. zerochat_runtime/assets/help/composer.html +134 -0
  26. zerochat_runtime/assets/help/conversations.html +105 -0
  27. zerochat_runtime/assets/help/debug.html +108 -0
  28. zerochat_runtime/assets/help/en/architecture.html +247 -0
  29. zerochat_runtime/assets/help/en/composer.html +134 -0
  30. zerochat_runtime/assets/help/en/conversations.html +104 -0
  31. zerochat_runtime/assets/help/en/debug.html +105 -0
  32. zerochat_runtime/assets/help/en/gemini-free.html +39 -0
  33. zerochat_runtime/assets/help/en/index.html +191 -0
  34. zerochat_runtime/assets/help/en/learning.html +71 -0
  35. zerochat_runtime/assets/help/en/mcp.html +143 -0
  36. zerochat_runtime/assets/help/en/openrouter-free.html +39 -0
  37. zerochat_runtime/assets/help/en/profiles.html +173 -0
  38. zerochat_runtime/assets/help/en/rag.html +130 -0
  39. zerochat_runtime/assets/help/en/reasoning-telemetry.html +108 -0
  40. zerochat_runtime/assets/help/en/tools-agent.html +133 -0
  41. zerochat_runtime/assets/help/en/webllm.html +355 -0
  42. zerochat_runtime/assets/help/gemini-free.html +39 -0
  43. zerochat_runtime/assets/help/help.css +604 -0
  44. zerochat_runtime/assets/help/help.js +51 -0
  45. zerochat_runtime/assets/help/index.html +194 -0
  46. zerochat_runtime/assets/help/learning.html +71 -0
  47. zerochat_runtime/assets/help/mcp.html +143 -0
  48. zerochat_runtime/assets/help/openrouter-free.html +39 -0
  49. zerochat_runtime/assets/help/profiles.html +174 -0
  50. zerochat_runtime/assets/help/rag.html +130 -0
  51. zerochat_runtime/assets/help/reasoning-telemetry.html +111 -0
  52. zerochat_runtime/assets/help/tools-agent.html +133 -0
  53. zerochat_runtime/assets/help/webllm.html +359 -0
  54. zerochat_runtime/assets/js/agent-core.js +1553 -0
  55. zerochat_runtime/assets/js/api.js +844 -0
  56. zerochat_runtime/assets/js/app.js +2558 -0
  57. zerochat_runtime/assets/js/attachments.js +220 -0
  58. zerochat_runtime/assets/js/charts.js +338 -0
  59. zerochat_runtime/assets/js/chat-engine.js +566 -0
  60. zerochat_runtime/assets/js/config-store.js +207 -0
  61. zerochat_runtime/assets/js/context-manager.js +584 -0
  62. zerochat_runtime/assets/js/conversation-service.js +484 -0
  63. zerochat_runtime/assets/js/cookies.js +688 -0
  64. zerochat_runtime/assets/js/data-reset-service.js +136 -0
  65. zerochat_runtime/assets/js/debug.js +508 -0
  66. zerochat_runtime/assets/js/defaults.js +16 -0
  67. zerochat_runtime/assets/js/export.js +179 -0
  68. zerochat_runtime/assets/js/file-parser.js +2088 -0
  69. zerochat_runtime/assets/js/generation-controller.js +417 -0
  70. zerochat_runtime/assets/js/i18n.js +1483 -0
  71. zerochat_runtime/assets/js/icons.js +156 -0
  72. zerochat_runtime/assets/js/ingestionEngine.js +436 -0
  73. zerochat_runtime/assets/js/markdown.js +588 -0
  74. zerochat_runtime/assets/js/mcp.js +1271 -0
  75. zerochat_runtime/assets/js/message-turns.js +62 -0
  76. zerochat_runtime/assets/js/profile-backup.js +118 -0
  77. zerochat_runtime/assets/js/profile-export-bundle.js +481 -0
  78. zerochat_runtime/assets/js/profile-repository.js +213 -0
  79. zerochat_runtime/assets/js/providers-webllm.js +486 -0
  80. zerochat_runtime/assets/js/providers.js +1560 -0
  81. zerochat_runtime/assets/js/rag-index.js +295 -0
  82. zerochat_runtime/assets/js/rag-service.js +530 -0
  83. zerochat_runtime/assets/js/rag-ui.js +997 -0
  84. zerochat_runtime/assets/js/ragStorage.js +676 -0
  85. zerochat_runtime/assets/js/sandbox.js +449 -0
  86. zerochat_runtime/assets/js/state.js +704 -0
  87. zerochat_runtime/assets/js/storage-db.js +144 -0
  88. zerochat_runtime/assets/js/tool-cards.js +330 -0
  89. zerochat_runtime/assets/js/tool-security.js +653 -0
  90. zerochat_runtime/assets/js/tools/README.md +50 -0
  91. zerochat_runtime/assets/js/tools/builtin/agent-checkpoint.tool.js +212 -0
  92. zerochat_runtime/assets/js/tools/builtin/download-pdf.tool.js +111 -0
  93. zerochat_runtime/assets/js/tools/builtin/execute-javascript.tool.js +219 -0
  94. zerochat_runtime/assets/js/tools/builtin/fetch-web-page.tool.js +112 -0
  95. zerochat_runtime/assets/js/tools/builtin/list-documents.tool.js +117 -0
  96. zerochat_runtime/assets/js/tools/builtin/read-knowledge-chunk.tool.js +129 -0
  97. zerochat_runtime/assets/js/tools/builtin/read-knowledge-image.tool.js +95 -0
  98. zerochat_runtime/assets/js/tools/builtin/render-chart.tool.js +90 -0
  99. zerochat_runtime/assets/js/tools/builtin/search-knowledge-base.tool.js +124 -0
  100. zerochat_runtime/assets/js/tools/builtin/search-web.tool.js +125 -0
  101. zerochat_runtime/assets/js/tools/tool-manifest.js +53 -0
  102. zerochat_runtime/assets/js/tools/tool-runtime.js +77 -0
  103. zerochat_runtime/assets/js/ui-composer.js +319 -0
  104. zerochat_runtime/assets/js/ui-conversation.js +655 -0
  105. zerochat_runtime/assets/js/ui-dialogs.js +132 -0
  106. zerochat_runtime/assets/js/ui-generation-status.js +119 -0
  107. zerochat_runtime/assets/js/ui-inspector.js +804 -0
  108. zerochat_runtime/assets/js/ui-mcp.js +604 -0
  109. zerochat_runtime/assets/js/ui-profiles.js +824 -0
  110. zerochat_runtime/assets/js/ui-reasoning.js +146 -0
  111. zerochat_runtime/assets/js/ui-settings.js +825 -0
  112. zerochat_runtime/assets/js/ui-shell.js +215 -0
  113. zerochat_runtime/assets/js/ui-sidebar.js +429 -0
  114. zerochat_runtime/assets/js/ui-telemetry.js +404 -0
  115. zerochat_runtime/assets/js/ui-transfer.js +262 -0
  116. zerochat_runtime/assets/js/utils.js +216 -0
  117. zerochat_runtime/assets/js/vendor/orama.browser.js +11 -0
  118. zerochat_runtime/assets/js/web-browser.js +569 -0
  119. zerochat_runtime/assets/js/web-search.js +519 -0
  120. zerochat_runtime/assets/manifest.webmanifest +20 -0
  121. zerochat_runtime/assets/services/dummy_mcp/dummy_mcp_server.py +47 -0
  122. zerochat_runtime/assets/services/dummy_mcp/service.json +22 -0
  123. zerochat_runtime/assets/services/lsp/installer.json +9 -0
  124. zerochat_runtime/assets/services/lsp/service.json +22 -0
  125. zerochat_runtime/assets/services/memory/installer.json +9 -0
  126. zerochat_runtime/assets/services/memory/service.json +24 -0
  127. zerochat_runtime/assets/services/playwright/installer.json +10 -0
  128. zerochat_runtime/assets/services/playwright/service.json +39 -0
  129. zerochat_runtime/assets/sw.js +164 -0
  130. zerochat_runtime/assets/zerochat.html +671 -0
@@ -0,0 +1,584 @@
1
+ /**
2
+ * Módulo de Gestión Inteligente del Contexto (ChatContextManager) para ZeroChat.
3
+ * Gestiona el presupuesto de tokens,
4
+ * ventana deslizante segura con preservación de pares agénticos, control de resultados de herramientas
5
+ * y sistema de compresión/resumen estructurado del historial.
6
+ */
7
+ (function (root, factory) {
8
+ if (typeof exports === 'object' && typeof module !== 'undefined') {
9
+ module.exports = factory();
10
+ } else {
11
+ root.ChatContextManager = factory();
12
+ }
13
+ }(typeof self !== 'undefined' ? self : this, function () {
14
+ 'use strict';
15
+
16
+ // ==========================================================================
17
+ // 1. Presupuestos y Ventanas de Contexto
18
+ // ==========================================================================
19
+
20
+ // Only a fallback. The authoritative value must come from the provider.
21
+ const DEFAULT_CONTEXT_LIMIT = 1000000;
22
+
23
+ /**
24
+ * Obtiene el límite de contexto publicado por el proveedor o el fallback general.
25
+ */
26
+ function getModelContextLimit(model = '', providerType = 'openai', configuredLimit) {
27
+ const limit = Number(configuredLimit);
28
+ return Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : DEFAULT_CONTEXT_LIMIT;
29
+ }
30
+
31
+ /**
32
+ * Calcula el presupuesto de entrada disponible deduciendo salida máxima y margen de seguridad.
33
+ */
34
+ function calculateInputBudget(options = {}) {
35
+ if (options.maxInputTokens && options.maxInputTokens > 0) {
36
+ return options.maxInputTokens;
37
+ }
38
+
39
+ const totalLimit = options.totalContextLimit || getModelContextLimit(options.model, options.providerType);
40
+ const maxOutput = options.maxOutputTokens || 4096;
41
+ const safetyMargin = Math.ceil(totalLimit * (options.safetyMarginRatio || 0.10));
42
+
43
+ const inputBudget = Math.max(1024, totalLimit - maxOutput - safetyMargin);
44
+ return inputBudget;
45
+ }
46
+
47
+ // ==========================================================================
48
+ // 2. Abstracción de Estimación de Tokens (Token Estimator)
49
+ // ==========================================================================
50
+
51
+ const customEstimators = new Map();
52
+
53
+ function registerEstimator(pattern, estimatorFn) {
54
+ if (typeof estimatorFn === 'function') {
55
+ customEstimators.set(String(pattern).toLowerCase(), estimatorFn);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Estimador genérico y tolerante para cadenas de texto.
61
+ * Aplica coeficientes según naturaleza del contenido (texto vs código/JSON).
62
+ */
63
+ function estimateTextTokens(text = '') {
64
+ if (!text || typeof text !== 'string') return 0;
65
+ const len = text.length;
66
+ if (len === 0) return 0;
67
+
68
+ // Código o JSON (densidad de caracteres de puntuación alta)
69
+ const isCodeOrJson = text.includes('{') || text.includes('function') || text.includes('const ') || text.includes('```');
70
+ const ratio = isCodeOrJson ? 2.9 : 3.6;
71
+
72
+ return Math.max(1, Math.ceil(len / ratio));
73
+ }
74
+
75
+ /**
76
+ * Estima los tokens de un único mensaje (incluyendo multimodales, tool_calls y tool results).
77
+ */
78
+ function estimateMessageTokens(message, model = '') {
79
+ if (!message || typeof message !== 'object') return 0;
80
+
81
+ // Comprobar si existe un estimador específico registrado para este modelo
82
+ const cleanModel = String(model || '').toLowerCase();
83
+ for (const [pattern, estimatorFn] of customEstimators.entries()) {
84
+ if (cleanModel.includes(pattern)) {
85
+ try {
86
+ return estimatorFn(message);
87
+ } catch (e) {
88
+ // Fallback silencioso
89
+ }
90
+ }
91
+ }
92
+
93
+ let tokens = 4; // Overhead por mensaje (role, estructura)
94
+
95
+ // Contenido textual o array multimodal
96
+ if (typeof message.content === 'string') {
97
+ tokens += estimateTextTokens(message.content);
98
+ } else if (Array.isArray(message.content)) {
99
+ message.content.forEach(part => {
100
+ if (!part) return;
101
+ if (part.type === 'text' && part.text) {
102
+ tokens += estimateTextTokens(part.text);
103
+ } else if (part.type === 'image_url' || part.type === 'image') {
104
+ // Estimación estándar para imágenes (alta resolución ~1200 tokens)
105
+ tokens += 1200;
106
+ }
107
+ });
108
+ }
109
+
110
+ // Tool calls emitidas por el asistente
111
+ if (Array.isArray(message.tool_calls)) {
112
+ message.tool_calls.forEach(tc => {
113
+ tokens += 10; // Overhead de tool call
114
+ if (tc.function) {
115
+ tokens += estimateTextTokens(tc.function.name || '');
116
+ const args = typeof tc.function.arguments === 'object'
117
+ ? JSON.stringify(tc.function.arguments)
118
+ : String(tc.function.arguments || '');
119
+ tokens += estimateTextTokens(args);
120
+ }
121
+ });
122
+ }
123
+
124
+ return tokens;
125
+ }
126
+
127
+ /**
128
+ * Estima los tokens totales de una lista de mensajes.
129
+ */
130
+ function estimateHistoryTokens(messages = [], model = '') {
131
+ if (!Array.isArray(messages)) return 0;
132
+ return messages.reduce((acc, m) => acc + estimateMessageTokens(m, model), 0);
133
+ }
134
+
135
+ // ==========================================================================
136
+ // 3. Control y Poda de Resultados de Herramientas (Tool Results)
137
+ // ==========================================================================
138
+
139
+ const DEFAULT_MAX_ACTIVE_TOOL_CHARS = 30000; // ~7.500 tokens para la herramienta del turno actual
140
+ const DEFAULT_MAX_HISTORICAL_TOOL_CHARS = 1200; // ~300 tokens para herramientas de turnos pasados
141
+
142
+ function serializeContent(content) {
143
+ if (typeof content === 'string') return content;
144
+ if (content === undefined) return '';
145
+ if (typeof content === 'object') return JSON.stringify(content);
146
+ return String(content);
147
+ }
148
+
149
+ /**
150
+ * Trunca de forma equilibrada una salida multisección (como múltiples fragmentos devueltos por read_knowledge_chunk).
151
+ * En lugar de cortar el 70% central de todo el texto (lo que elimina por completo fragmentos enteros del medio),
152
+ * distribuye el presupuesto proporcionalmente entre las secciones para que cada fragmento conserve su cabecera,
153
+ * contexto y datos numéricos clave.
154
+ */
155
+ function truncateMultiSectionToolContent(sections, maxChars, toolName) {
156
+ const separator = '\n\n---\n\n';
157
+ const totalSeparatorsLength = separator.length * (sections.length - 1);
158
+ const availableBudget = Math.max(sections.length * 200, maxChars - totalSeparatorsLength);
159
+
160
+ const initialShare = Math.floor(availableBudget / sections.length);
161
+ let remainingBudget = availableBudget;
162
+ const finalSections = new Array(sections.length);
163
+ const oversizedIndices = [];
164
+
165
+ sections.forEach((sec, idx) => {
166
+ if (sec.length <= initialShare) {
167
+ finalSections[idx] = sec;
168
+ remainingBudget -= sec.length;
169
+ } else {
170
+ oversizedIndices.push(idx);
171
+ }
172
+ });
173
+
174
+ if (oversizedIndices.length === 0) {
175
+ return sections.join(separator);
176
+ }
177
+
178
+ const shareForOversized = Math.max(150, Math.floor(remainingBudget / oversizedIndices.length));
179
+ oversizedIndices.forEach(idx => {
180
+ const sec = sections[idx];
181
+ if (sec.length <= shareForOversized) {
182
+ finalSections[idx] = sec;
183
+ } else {
184
+ const headLen = Math.floor(shareForOversized * 0.7);
185
+ const tailLen = Math.floor(shareForOversized * 0.2);
186
+ const head = sec.slice(0, headLen);
187
+ const tail = sec.slice(-tailLen);
188
+ const omitted = sec.length - (headLen + tailLen);
189
+ finalSections[idx] = `${head}\n\n[... Truncado fragmento ${idx + 1}: ${omitted} caracteres omitidos de ${toolName} ...]\n\n${tail}`;
190
+ }
191
+ });
192
+
193
+ return finalSections.join(separator);
194
+ }
195
+
196
+ /**
197
+ * Trunca de forma segura el contenido de un resultado de herramienta.
198
+ */
199
+ function truncateToolContent(content, maxChars = DEFAULT_MAX_ACTIVE_TOOL_CHARS, toolName = 'tool') {
200
+ const str = serializeContent(content);
201
+ if (str.length <= maxChars) {
202
+ return str;
203
+ }
204
+
205
+ // Si el contenido contiene múltiples secciones separadas por '---' (típico de read_knowledge_chunk)
206
+ if (str.includes('\n\n---\n\n')) {
207
+ const sections = str.split('\n\n---\n\n');
208
+ if (sections.length > 1) {
209
+ return truncateMultiSectionToolContent(sections, maxChars, toolName);
210
+ }
211
+ }
212
+
213
+ const head = str.slice(0, Math.floor(maxChars * 0.7));
214
+ const tail = str.slice(-Math.floor(maxChars * 0.2));
215
+ return `${head}\n\n[... Truncado por ChatContextManager: ${str.length - maxChars} caracteres omitidos de la salida de ${toolName} ...]\n\n${tail}`;
216
+ }
217
+
218
+ /**
219
+ * Compacta y poda los resultados de herramientas de turnos pasados completados.
220
+ */
221
+ function pruneHistoricalToolMessage(m, maxHistoricalToolChars = DEFAULT_MAX_HISTORICAL_TOOL_CHARS) {
222
+ if (!m || m.role !== 'tool') return m;
223
+ const contentStr = serializeContent(m.content);
224
+
225
+ if (contentStr.length <= maxHistoricalToolChars) {
226
+ return m;
227
+ }
228
+
229
+ const truncated = truncateToolContent(contentStr, maxHistoricalToolChars, m.name || 'tool');
230
+ return {
231
+ ...m,
232
+ content: truncated,
233
+ _prunedByContextManager: true
234
+ };
235
+ }
236
+
237
+ // ==========================================================================
238
+ // 4. Ventana Deslizante con Preservación de Pares Agénticos (Pair-Safe Sliding Window)
239
+ // ==========================================================================
240
+
241
+ /**
242
+ * Agrupa los mensajes en bloques atómicos indivisibles para no romper sintaxis de Function Calling.
243
+ * Un bloque puede ser:
244
+ * - Un mensaje regular de usuario o asistente.
245
+ * - Un par asistente (con tool_calls) + sus correspondientes mensajes tool (role: 'tool').
246
+ */
247
+ function groupIntoAtomicBlocks(messages = []) {
248
+ const blocks = [];
249
+ let i = 0;
250
+
251
+ while (i < messages.length) {
252
+ const current = messages[i];
253
+
254
+ // Caso 1: Asistente con llamadas a herramientas
255
+ if (current.role === 'assistant' && Array.isArray(current.tool_calls) && current.tool_calls.length > 0) {
256
+ const block = [current];
257
+ i++;
258
+ // Recoger todos los mensajes 'tool' consecutivos que responden a este assistant
259
+ while (i < messages.length && messages[i].role === 'tool') {
260
+ block.push(messages[i]);
261
+ i++;
262
+ }
263
+ blocks.push(block);
264
+ continue;
265
+ }
266
+
267
+ // Caso 2: Mensaje tool huérfano (se protege en un bloque individual)
268
+ if (current.role === 'tool') {
269
+ blocks.push([current]);
270
+ i++;
271
+ continue;
272
+ }
273
+
274
+ // Caso 3: Mensaje regular de usuario, asistente o sistema
275
+ blocks.push([current]);
276
+ i++;
277
+ }
278
+
279
+ return blocks;
280
+ }
281
+
282
+ /**
283
+ * Construye el contexto optimizado aplicando presupuesto, poda y ventana deslizante.
284
+ */
285
+ function buildOptimizedContext(rawMessages = [], options = {}) {
286
+ const model = options.model || '';
287
+ const providerType = options.providerType || 'openai';
288
+ const inputBudget = calculateInputBudget(options);
289
+ const maxHistoricalToolChars = options.maxHistoricalToolChars || DEFAULT_MAX_HISTORICAL_TOOL_CHARS;
290
+ const maxActiveToolChars = options.maxActiveToolChars || DEFAULT_MAX_ACTIVE_TOOL_CHARS;
291
+
292
+ if (!Array.isArray(rawMessages) || rawMessages.length === 0) {
293
+ return {
294
+ messages: [],
295
+ diagnostics: {
296
+ budget: inputBudget,
297
+ totalTokens: 0,
298
+ includedCount: 0,
299
+ excludedCount: 0,
300
+ prunedToolsCount: 0
301
+ }
302
+ };
303
+ }
304
+
305
+ // 1. Separar mensajes del Sistema (Header crítico) y bloques de Memoria
306
+ const systemMessages = [];
307
+ const conversationMessages = [];
308
+
309
+ rawMessages.forEach(m => {
310
+ if (m && m.role === 'system') {
311
+ systemMessages.push(m);
312
+ } else if (m && m.role) {
313
+ conversationMessages.push(m);
314
+ }
315
+ });
316
+
317
+ // 2. Si no hay conversación, retornar solo el sistema
318
+ if (conversationMessages.length === 0) {
319
+ const systemTokens = estimateHistoryTokens(systemMessages, model);
320
+ return {
321
+ messages: systemMessages,
322
+ diagnostics: {
323
+ budget: inputBudget,
324
+ totalTokens: systemTokens,
325
+ systemTokens,
326
+ includedCount: systemMessages.length,
327
+ excludedCount: 0,
328
+ prunedToolsCount: 0
329
+ }
330
+ };
331
+ }
332
+
333
+ // 3. Separar el Último Turno (Footer crítico que jamás se elimina)
334
+ // El último turno incluye el último bloque atómico (ej: último user prompt o tool en curso)
335
+ const atomicBlocks = groupIntoAtomicBlocks(conversationMessages);
336
+ const lastBlock = atomicBlocks.pop(); // Último bloque indispensable
337
+
338
+ // Aplicar límite al bloque activo si contiene herramientas
339
+ const isExplicitActiveToolChars = typeof options.maxActiveToolChars === 'number';
340
+ const processedLastBlock = lastBlock.map(m => {
341
+ if (m.role === 'tool') {
342
+ const isReadKnowledge = !isExplicitActiveToolChars && (m.name === 'read_knowledge_chunk' || m.name === 'readknowledgechunk');
343
+ const effectiveMaxActiveChars = isReadKnowledge
344
+ ? Math.max(maxActiveToolChars, Math.min(45000, Math.floor(inputBudget * 1.5)))
345
+ : maxActiveToolChars;
346
+ return {
347
+ ...m,
348
+ content: truncateToolContent(m.content, effectiveMaxActiveChars, m.name)
349
+ };
350
+ }
351
+ return m;
352
+ });
353
+
354
+ const systemTokens = estimateHistoryTokens(systemMessages, model);
355
+ const lastBlockTokens = estimateHistoryTokens(processedLastBlock, model);
356
+
357
+ let currentTokens = systemTokens + lastBlockTokens;
358
+ const remainingBudget = Math.max(0, inputBudget - currentTokens);
359
+
360
+ // 4. Poda de herramientas pasadas en los bloques históricos
361
+ let prunedToolsCount = 0;
362
+ const processedHistoricalBlocks = atomicBlocks.map(block => {
363
+ return block.map(m => {
364
+ if (m.role === 'tool') {
365
+ const pruned = pruneHistoricalToolMessage(m, maxHistoricalToolChars);
366
+ if (pruned._prunedByContextManager) prunedToolsCount++;
367
+ return pruned;
368
+ }
369
+ return m;
370
+ });
371
+ });
372
+
373
+ // 5. Ventana deslizante hacia atrás (de más reciente a más antiguo)
374
+ const includedHistoricalBlocks = [];
375
+ let excludedMessagesCount = 0;
376
+
377
+ for (let bIdx = processedHistoricalBlocks.length - 1; bIdx >= 0; bIdx--) {
378
+ const block = processedHistoricalBlocks[bIdx];
379
+ const blockTokens = estimateHistoryTokens(block, model);
380
+
381
+ if (currentTokens + blockTokens <= inputBudget) {
382
+ includedHistoricalBlocks.unshift(block);
383
+ currentTokens += blockTokens;
384
+ } else {
385
+ // Bloque no cabe en el presupuesto: se excluye completo
386
+ excludedMessagesCount += block.length;
387
+ }
388
+ }
389
+
390
+ // 6. Ensamblado final de la lista de mensajes
391
+ const finalMessages = [
392
+ ...systemMessages,
393
+ ...includedHistoricalBlocks.flat(),
394
+ ...processedLastBlock
395
+ ];
396
+
397
+ const totalFinalTokens = estimateHistoryTokens(finalMessages, model);
398
+
399
+ return {
400
+ messages: finalMessages,
401
+ diagnostics: {
402
+ budget: inputBudget,
403
+ totalTokens: totalFinalTokens,
404
+ systemTokens: systemTokens,
405
+ includedCount: finalMessages.length,
406
+ excludedCount: excludedMessagesCount,
407
+ prunedToolsCount: prunedToolsCount,
408
+ strategy: excludedMessagesCount > 0 ? 'sliding_window_truncated' : 'full_history'
409
+ }
410
+ };
411
+ }
412
+
413
+ // ==========================================================================
414
+ // 5. Sistema de Compresión y Resumen Inteligente de Memoria (Memory Compression)
415
+ // ==========================================================================
416
+
417
+ const SUMMARIZER_SYSTEM_PROMPT = `You consolidate a conversation checkpoint. Create one concise, cumulative summary of the checkpoint supplied, if any, and every subsequent message supplied. Preserve user goals, decisions, verified facts, tool calls and their results, open questions, and details needed to continue. Do not mention this instruction or omit relevant information merely because of its source.`;
418
+
419
+ /**
420
+ * Evalúa si una conversación amerita compresión según presupuesto y volumen de turnos.
421
+ */
422
+ function shouldCompress(messages = [], options = {}) {
423
+ if (!Array.isArray(messages) || messages.length < 8) {
424
+ return false;
425
+ }
426
+
427
+ const minMessagesToCompress = options.minMessagesToCompress || 2;
428
+ const lastCheckpointIndex = messages.reduce((lastIndex, message, index) => (
429
+ message && message._isSummaryBlock ? index : lastIndex
430
+ ), -1);
431
+ const messagesSinceCheckpoint = messages.slice(lastCheckpointIndex + 1)
432
+ .filter(message => message && message.role && message.role !== 'system');
433
+
434
+ if (messagesSinceCheckpoint.length < minMessagesToCompress) {
435
+ return false;
436
+ }
437
+
438
+ const model = options.model || '';
439
+ const inputBudget = calculateInputBudget(options);
440
+ const totalTokens = estimateHistoryTokens(messages, model);
441
+ const thresholdRatio = options.compressionThresholdRatio || 0.70;
442
+
443
+ return totalTokens >= (inputBudget * thresholdRatio);
444
+ }
445
+
446
+ /**
447
+ * Construye el prompt de transcripción para enviar al motor de resumen.
448
+ */
449
+ /**
450
+ * Comprime y consolida de forma segura los turnos antiguos de una conversación.
451
+ */
452
+ async function compressHistory(params = {}) {
453
+ const {
454
+ messages = [],
455
+ summarizeFn = null,
456
+ options = {}
457
+ } = params;
458
+
459
+ if (!Array.isArray(messages) || messages.length === 0) {
460
+ return { messages: [], compressed: false, reason: 'empty_messages' };
461
+ }
462
+
463
+ // Preserve permanent system messages. A checkpoint is the only historical
464
+ // memory passed to the summarizer; every later message is passed verbatim.
465
+ const systemMessages = [];
466
+ let checkpoint = null;
467
+ let lastCheckpointIndex = -1;
468
+
469
+ messages.forEach((m, index) => {
470
+ if (m && m._isSummaryBlock) {
471
+ checkpoint = m;
472
+ lastCheckpointIndex = index;
473
+ } else if (m && m.role === 'system') {
474
+ systemMessages.push(m);
475
+ }
476
+ });
477
+
478
+ const dialogue = messages.slice(lastCheckpointIndex + 1).filter(m => m && m.role && m.role !== 'system');
479
+ if (dialogue.length === 0 || typeof summarizeFn !== 'function') {
480
+ return { messages, compressed: false, reason: dialogue.length === 0 ? 'no_new_dialogue' : 'summarizer_unavailable' };
481
+ }
482
+
483
+ const originalTokens = estimateHistoryTokens([...(checkpoint ? [checkpoint] : []), ...dialogue], options.model);
484
+ let summaryContent = '';
485
+ try {
486
+ summaryContent = await summarizeFn({
487
+ systemPrompt: SUMMARIZER_SYSTEM_PROMPT,
488
+ checkpoint,
489
+ dialogue,
490
+ messages: [...(checkpoint ? [checkpoint] : []), ...dialogue]
491
+ });
492
+ } catch (err) {
493
+ return { messages, compressed: false, reason: 'summarization_failed', error: err };
494
+ }
495
+
496
+ if (typeof summaryContent !== 'string' || summaryContent.trim() === '') {
497
+ return { messages, compressed: false, reason: 'empty_summary' };
498
+ }
499
+
500
+ // 5. Construir el bloque de memoria sintético
501
+ const memoryBlock = {
502
+ id: `summary_${Date.now()}`,
503
+ role: 'system',
504
+ content: summaryContent,
505
+ _isSummaryBlock: true,
506
+ _compressedMetadata: {
507
+ timestamp: Date.now(),
508
+ originalMessagesCount: (checkpoint ? 1 : 0) + dialogue.length,
509
+ originalEstimatedTokens: originalTokens,
510
+ replacesThroughIndex: messages.length - 1
511
+ }
512
+ };
513
+
514
+ // 6. Ensamblar la nueva historia comprimida
515
+ const newMessages = [
516
+ ...systemMessages,
517
+ memoryBlock,
518
+ ];
519
+
520
+ const compressedTokens = estimateHistoryTokens(newMessages, options.model);
521
+
522
+ return {
523
+ messages: newMessages,
524
+ compressed: true,
525
+ memoryBlock,
526
+ diagnostics: {
527
+ originalMessagesCount: messages.length,
528
+ newMessagesCount: newMessages.length,
529
+ originalTokens,
530
+ compressedTokens,
531
+ savedTokens: Math.max(0, originalTokens - estimateMessageTokens(memoryBlock, options.model))
532
+ }
533
+ };
534
+ }
535
+
536
+ /**
537
+ * Calcula diagnósticos del estado del contexto para telemetría e interfaz de usuario.
538
+ * @param {Array} messages - Historial de mensajes.
539
+ * @param {Object} [options={}] - Parámetros de modelo, proveedor y tokens informados.
540
+ * @returns {Object} Diagnósticos normalizados.
541
+ */
542
+ function getContextDiagnostics(messages = [], options = {}) {
543
+ const model = options.model || '';
544
+ const providerType = options.providerType || 'openai';
545
+ const totalLimit = options.totalContextLimit || getModelContextLimit(model, providerType);
546
+ const budget = calculateInputBudget({ ...options, totalContextLimit: totalLimit });
547
+
548
+ const isEstimated = options.usedTokens === undefined || options.usedTokens === null;
549
+ const usedTokens = !isEstimated ? Number(options.usedTokens) : estimateHistoryTokens(messages, model);
550
+ const percentUsed = totalLimit > 0 ? Math.min(100, (usedTokens / totalLimit) * 100) : 0;
551
+ const remainingTokens = Math.max(0, totalLimit - usedTokens);
552
+
553
+ return {
554
+ model,
555
+ providerType,
556
+ totalLimit,
557
+ budget,
558
+ usedTokens,
559
+ remainingTokens,
560
+ percentUsed: Number(percentUsed.toFixed(1)),
561
+ isEstimated,
562
+ prunedCount: options.prunedCount || 0,
563
+ excludedCount: options.excludedCount || 0,
564
+ strategy: options.strategy || (options.excludedCount > 0 ? 'sliding_window_truncated' : 'full_history')
565
+ };
566
+ }
567
+
568
+ return {
569
+ DEFAULT_CONTEXT_LIMIT,
570
+ getModelContextLimit,
571
+ calculateInputBudget,
572
+ estimateTextTokens,
573
+ estimateMessageTokens,
574
+ estimateHistoryTokens,
575
+ registerEstimator,
576
+ truncateToolContent,
577
+ pruneHistoricalToolMessage,
578
+ groupIntoAtomicBlocks,
579
+ buildOptimizedContext,
580
+ getContextDiagnostics,
581
+ shouldCompress,
582
+ compressHistory
583
+ };
584
+ }));