openprompt-lang 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +2 -0
- package/docs/00-ARCHITECTURE/OPL-BOOST-MULTI-AGENT.md +406 -0
- package/docs/02-STANDARDS/AGENTS.template.md +89 -0
- package/docs/02-STANDARDS/ticket-driven-development.md +99 -0
- package/docs/04-TICKETS/BOOST-001-profile-registry.md +66 -0
- package/docs/04-TICKETS/BOOST-002-context-compression.md +58 -0
- package/docs/04-TICKETS/BOOST-003-template-hydration.md +69 -0
- package/docs/04-TICKETS/BOOST-004-fewshot-engine.md +58 -0
- package/docs/04-TICKETS/BOOST-005-agent-pool.md +69 -0
- package/docs/04-TICKETS/BOOST-006-specialized-agents.md +53 -0
- package/docs/04-TICKETS/BOOST-007-validation-loop.md +56 -0
- package/docs/04-TICKETS/BOOST-008-orchestrator.md +71 -0
- package/docs/04-TICKETS/BOOST-009-cache-system.md +56 -0
- package/docs/04-TICKETS/BOOST-010-cli-mcp.md +67 -0
- package/docs/04-TICKETS/BOOST-011-self-learning.md +50 -0
- package/docs/04-TICKETS/BOOST-012-prompt-preamble.md +109 -0
- package/docs/04-TICKETS/BOOST-013-hydrator-duplicate-code.md +132 -0
- package/docs/04-TICKETS/BOOST-014-multiagent-missing-parts.md +87 -0
- package/docs/04-TICKETS/BOOST-015-skeleton-type-missing.md +76 -0
- package/docs/04-TICKETS/BOOST-016-output-path-duplicate.md +68 -0
- package/docs/04-TICKETS/INDEX.md +89 -0
- package/docs/04-TICKETS/_archive/BOOST-005-micro-tasking.md +67 -0
- package/docs/04-TICKETS/_archive/BOOST-006-validation-loop.md +66 -0
- package/docs/04-TICKETS/_archive/BOOST-007-progressive-pipeline.md +69 -0
- package/docs/04-TICKETS/_archive/BOOST-008-cli-mcp-integration.md +74 -0
- package/docs/AI_CONTEXT.md +16 -0
- package/package.json +3 -2
- package/src/boost/agent-pool.js +442 -0
- package/src/boost/agents/index.js +79 -0
- package/src/boost/cache.js +241 -0
- package/src/boost/context-compressor.js +354 -0
- package/src/boost/fewshot-retriever.js +332 -0
- package/src/boost/hardware-detector.js +486 -0
- package/src/boost/hydrator.js +398 -0
- package/src/boost/index.js +60 -0
- package/src/boost/orchestrator.js +615 -0
- package/src/boost/preamble.js +217 -0
- package/src/boost/profile-registry.js +264 -0
- package/src/boost/self-learn.js +247 -0
- package/src/boost/skeletons/component.skeleton.js +24 -0
- package/src/boost/skeletons/hook.skeleton.js +27 -0
- package/src/boost/skeletons/index.js +67 -0
- package/src/boost/skeletons/page.skeleton.js +22 -0
- package/src/boost/skeletons/service.skeleton.js +20 -0
- package/src/boost/skeletons/store.skeleton.js +18 -0
- package/src/boost/skeletons/type.skeleton.js +11 -0
- package/src/boost/task-dispatcher.js +142 -0
- package/src/boost/validation-loop.js +495 -0
- package/src/cli/commands-boost.js +394 -0
- package/src/mcp-refactor/handlers/boost.js +295 -0
- package/src/mcp-refactor/router.js +19 -0
- package/src/mcp-refactor/tools.js +113 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
// @use(kind, contract, limit)
|
|
2
|
+
// @kind(util)
|
|
3
|
+
// @contract(in: none -> out: detectHardware, getRuntimeStatus, getSafeParallelism, getRAMWarning, generateBoostConfig, saveBoostConfig, getSetupMessage)
|
|
4
|
+
// @limit(lines: 350)
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hardware Detector — Módulo OPL Boost
|
|
8
|
+
*
|
|
9
|
+
* Detecta las capacidades del PC del usuario para recomendar
|
|
10
|
+
* la configuración óptima de Boost: perfil, paralelismo, modelo.
|
|
11
|
+
*
|
|
12
|
+
* Se ejecuta en:
|
|
13
|
+
* - Primera instalación (postinstall / opl boost setup)
|
|
14
|
+
* - Bajo demanda (opl boost setup --re-detect)
|
|
15
|
+
*
|
|
16
|
+
* La configuración se guarda en prompt-lang.json > boost.hardware
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import os from "os"
|
|
20
|
+
import { execSync } from "child_process"
|
|
21
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"
|
|
22
|
+
import { join } from "path"
|
|
23
|
+
import { PROFILES } from "./profile-registry.js"
|
|
24
|
+
|
|
25
|
+
// ──────────────────────────────────────────────
|
|
26
|
+
// Detección de hardware
|
|
27
|
+
// ──────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
function bytesToGB(bytes) {
|
|
30
|
+
return Math.round(bytes / (1024 * 1024 * 1024))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function detectRAM() {
|
|
34
|
+
return {
|
|
35
|
+
totalBytes: os.totalmem(),
|
|
36
|
+
totalGB: bytesToGB(os.totalmem()),
|
|
37
|
+
freeBytes: os.freemem(),
|
|
38
|
+
freeGB: bytesToGB(os.freemem()),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function detectCPU() {
|
|
43
|
+
const cpus = os.cpus()
|
|
44
|
+
return {
|
|
45
|
+
cores: cpus.length,
|
|
46
|
+
model: cpus[0]?.model?.trim() || "Desconocido",
|
|
47
|
+
arch: process.arch,
|
|
48
|
+
platform: process.platform,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function detectGPU() {
|
|
53
|
+
try {
|
|
54
|
+
if (process.platform === "win32") {
|
|
55
|
+
const output = execSync(
|
|
56
|
+
'wmic path win32_VideoController get name,adapterram 2>nul',
|
|
57
|
+
{ encoding: "utf-8", timeout: 5000 }
|
|
58
|
+
)
|
|
59
|
+
const lines = output.split("\n").filter((l) => l.trim() && !l.includes("Name"))
|
|
60
|
+
if (lines.length > 0) {
|
|
61
|
+
const parts = lines[0].trim().split(/\s{2,}/)
|
|
62
|
+
return {
|
|
63
|
+
name: parts[0] || "GPU detectada",
|
|
64
|
+
vramBytes: parts[1] ? parseInt(parts[1]) : null,
|
|
65
|
+
vramGB: parts[1] ? bytesToGB(parseInt(parts[1])) : null,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
// Linux / macOS — nvidia-smi
|
|
70
|
+
const output = execSync(
|
|
71
|
+
'nvidia-smi --query-gpu=name,memory.total --format=csv,noheader 2>/dev/null || echo ""',
|
|
72
|
+
{ encoding: "utf-8", timeout: 5000 }
|
|
73
|
+
)
|
|
74
|
+
const line = output.trim()
|
|
75
|
+
if (line) {
|
|
76
|
+
const [name, memStr] = line.split(", ")
|
|
77
|
+
const vramMB = memStr ? parseInt(memStr.replace(" MiB", "").trim()) : null
|
|
78
|
+
return {
|
|
79
|
+
name: name?.trim() || "NVIDIA GPU detectada",
|
|
80
|
+
vramBytes: vramMB ? vramMB * 1024 * 1024 : null,
|
|
81
|
+
vramGB: vramMB ? Math.round(vramMB / 1024) : null,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Intel ARC / AMD — intentar con glxinfo
|
|
86
|
+
const glxOutput = execSync(
|
|
87
|
+
'glxinfo -B 2>/dev/null | grep "Device:" || echo ""',
|
|
88
|
+
{ encoding: "utf-8", timeout: 5000 }
|
|
89
|
+
)
|
|
90
|
+
if (glxOutput.trim()) {
|
|
91
|
+
return { name: glxOutput.trim().replace("Device:", "").trim(), vramBytes: null, vramGB: null }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// No GPU detectada o no hay drivers
|
|
96
|
+
}
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function detectOllamaStatus() {
|
|
101
|
+
try {
|
|
102
|
+
const output = execSync(
|
|
103
|
+
'curl -s http://localhost:11434/api/tags 2>/dev/null || echo ""',
|
|
104
|
+
{ encoding: "utf-8", timeout: 3000 }
|
|
105
|
+
)
|
|
106
|
+
if (output) {
|
|
107
|
+
const data = JSON.parse(output)
|
|
108
|
+
const models = (data.models || []).map((m) => ({
|
|
109
|
+
name: m.name,
|
|
110
|
+
sizeGB: Math.round(m.size / (1024 * 1024 * 1024) * 10) / 10,
|
|
111
|
+
}))
|
|
112
|
+
return { installed: true, models }
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// Ollama no está corriendo
|
|
116
|
+
}
|
|
117
|
+
return { installed: false, models: [] }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ──────────────────────────────────────────────
|
|
121
|
+
// Recomendación de configuración
|
|
122
|
+
// ──────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
function recommendProfile(ram, cpu, gpu) {
|
|
125
|
+
const ramGB = ram.totalGB
|
|
126
|
+
const cores = cpu.cores
|
|
127
|
+
const hasGPU = gpu !== null
|
|
128
|
+
const vramGB = gpu?.vramGB || 0
|
|
129
|
+
|
|
130
|
+
// Reglas de recomendación basadas en hardware real
|
|
131
|
+
if (ramGB >= 32 && cores >= 8 && hasGPU && vramGB >= 8) {
|
|
132
|
+
return { profile: "large", reason: "PC de alta capacidad: RAM ≥32GB, GPU ≥8GB VRAM, ≥8 núcleos" }
|
|
133
|
+
}
|
|
134
|
+
if (ramGB >= 16 && cores >= 6) {
|
|
135
|
+
return { profile: "medium", reason: "PC de capacidad media: RAM ≥16GB, ≥6 núcleos" }
|
|
136
|
+
}
|
|
137
|
+
if (ramGB >= 8 && cores >= 4) {
|
|
138
|
+
return { profile: "medium", reason: "PC estándar: RAM ≥8GB, ≥4 núcleos. Se usará modo cola para no saturar." }
|
|
139
|
+
}
|
|
140
|
+
return { profile: "small", reason: "PC con recursos limitados: se usará el modelo más pequeño y modo cola" }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function recommendParallelism(ram, gpu, profile) {
|
|
144
|
+
const ramGB = ram.totalGB
|
|
145
|
+
const hasGPU = gpu !== null
|
|
146
|
+
const vramGB = gpu?.vramGB || 0
|
|
147
|
+
|
|
148
|
+
if (profile === "large" && hasGPU && vramGB >= 12) {
|
|
149
|
+
return { mode: "parallel", maxConcurrency: 3, reason: "GPU con suficiente VRAM para 3 instancias" }
|
|
150
|
+
}
|
|
151
|
+
if (profile === "large" && hasGPU) {
|
|
152
|
+
return { mode: "parallel", maxConcurrency: 2, reason: "GPU disponible, 2 instancias en paralelo" }
|
|
153
|
+
}
|
|
154
|
+
if (ramGB >= 32 && hasGPU) {
|
|
155
|
+
return { mode: "parallel", maxConcurrency: 2, reason: "Suficiente RAM y GPU para 2 instancias" }
|
|
156
|
+
}
|
|
157
|
+
if (ramGB >= 16) {
|
|
158
|
+
return { mode: "queue", maxConcurrency: 1, reason: "RAM suficiente para cola, no para paralelo real" }
|
|
159
|
+
}
|
|
160
|
+
return { mode: "queue", maxConcurrency: 1, reason: "Modo cola: un agente a la vez para no saturar el sistema" }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function recommendModel(gpu, ram, profile) {
|
|
164
|
+
const hasGPU = gpu !== null
|
|
165
|
+
const vramGB = gpu?.vramGB || 0
|
|
166
|
+
|
|
167
|
+
if (profile === "large" && hasGPU && vramGB >= 16) {
|
|
168
|
+
return "qwen2.5-coder:7b" // o 14b si está disponible
|
|
169
|
+
}
|
|
170
|
+
if (profile === "medium" && hasGPU && vramGB >= 6) {
|
|
171
|
+
return "qwen2.5-coder:7b"
|
|
172
|
+
}
|
|
173
|
+
if (profile === "medium" && ram.totalGB >= 16) {
|
|
174
|
+
return "qwen2.5-coder:7b" // CPU con suficiente RAM
|
|
175
|
+
}
|
|
176
|
+
return "llama3.2:latest" // Modelo pequeño para CPU o RAM limitada
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ──────────────────────────────────────────────
|
|
180
|
+
// API pública
|
|
181
|
+
// ──────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
export function detectHardware() {
|
|
184
|
+
const ram = detectRAM()
|
|
185
|
+
const cpu = detectCPU()
|
|
186
|
+
const gpu = detectGPU()
|
|
187
|
+
const ollama = detectOllamaStatus()
|
|
188
|
+
const profileRec = recommendProfile(ram, cpu, gpu)
|
|
189
|
+
const parallelRec = recommendParallelism(ram, gpu, profileRec.profile)
|
|
190
|
+
const modelRec = recommendModel(gpu, ram, profileRec.profile)
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
timestamp: new Date().toISOString(),
|
|
194
|
+
os: `${process.platform} ${process.arch}`,
|
|
195
|
+
ram,
|
|
196
|
+
cpu,
|
|
197
|
+
gpu,
|
|
198
|
+
ollama,
|
|
199
|
+
recommendation: {
|
|
200
|
+
profile: profileRec.profile,
|
|
201
|
+
profileReason: profileRec.reason,
|
|
202
|
+
agentMode: parallelRec.mode,
|
|
203
|
+
maxConcurrency: parallelRec.maxConcurrency,
|
|
204
|
+
concurrencyReason: parallelRec.reason,
|
|
205
|
+
recommendedModel: modelRec,
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function generateBoostConfig(hardware) {
|
|
211
|
+
const { recommendation, ollama } = hardware
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
enabled: true,
|
|
215
|
+
defaultProfile: "auto",
|
|
216
|
+
hardware: {
|
|
217
|
+
totalRamGB: hardware.ram.totalGB,
|
|
218
|
+
cpuCores: hardware.cpu.cores,
|
|
219
|
+
gpuName: hardware.gpu?.name || null,
|
|
220
|
+
gpuVramGB: hardware.gpu?.vramGB || null,
|
|
221
|
+
hasOllama: ollama.installed,
|
|
222
|
+
ollamaModels: ollama.models.map((m) => m.name),
|
|
223
|
+
detectedAt: hardware.timestamp,
|
|
224
|
+
},
|
|
225
|
+
profiles: {
|
|
226
|
+
small: PROFILES.small,
|
|
227
|
+
medium: PROFILES.medium,
|
|
228
|
+
large: PROFILES.large,
|
|
229
|
+
},
|
|
230
|
+
agentPool: {
|
|
231
|
+
mode: recommendation.agentMode,
|
|
232
|
+
maxConcurrency: recommendation.maxConcurrency,
|
|
233
|
+
modelName: recommendation.recommendedModel,
|
|
234
|
+
timeout: 30000,
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function saveBoostConfig(boostConfig) {
|
|
240
|
+
const configPath = join(process.cwd(), "prompt-lang.json")
|
|
241
|
+
|
|
242
|
+
let config
|
|
243
|
+
if (existsSync(configPath)) {
|
|
244
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"))
|
|
245
|
+
} else {
|
|
246
|
+
config = {}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
config.boost = boostConfig
|
|
250
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8")
|
|
251
|
+
return configPath
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function getSetupMessage(hardware) {
|
|
255
|
+
const { ram, cpu, gpu, ollama, recommendation } = hardware
|
|
256
|
+
const lines = []
|
|
257
|
+
|
|
258
|
+
lines.push("")
|
|
259
|
+
lines.push("╔══════════════════════════════════════════════════════════╗")
|
|
260
|
+
lines.push("║ 🚀 OPL Boost — Setup de Hardware ║")
|
|
261
|
+
lines.push("╚══════════════════════════════════════════════════════════╝")
|
|
262
|
+
lines.push("")
|
|
263
|
+
|
|
264
|
+
lines.push("📊 Hardware detectado:")
|
|
265
|
+
lines.push(` RAM: ${ram.totalGB} GB (${ram.freeGB} GB libres)`)
|
|
266
|
+
lines.push(` CPU: ${cpu.cores} núcleos — ${cpu.model}`)
|
|
267
|
+
lines.push(` GPU: ${gpu ? `${gpu.name} (${gpu.vramGB ? gpu.vramGB + " GB VRAM" : "desconocida"})` : "No detectada"}`)
|
|
268
|
+
|
|
269
|
+
if (ollama.installed) {
|
|
270
|
+
lines.push(` 🟢 Ollama: INSTALADO (${ollama.models.length} modelos disponibles)`)
|
|
271
|
+
for (const m of ollama.models) {
|
|
272
|
+
lines.push(` • ${m.name} (${m.sizeGB} GB)`)
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
lines.push(` 🔴 Ollama: NO DETECTADO`)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
lines.push("")
|
|
279
|
+
lines.push("💡 ¿Por qué necesitamos Ollima?")
|
|
280
|
+
lines.push(" OPL Boost usa modelos locales de IA para generar código.")
|
|
281
|
+
lines.push(" En lugar de gastar tokens de un modelo grande (GPT-4, Claude),")
|
|
282
|
+
lines.push(" Boost reparte el trabajo entre modelos pequeños (7B) que corren")
|
|
283
|
+
lines.push(" en tu PC. Es más lento pero MUCHO más barato, y te permite")
|
|
284
|
+
lines.push(" trabajar sin conexión ni límites de API.")
|
|
285
|
+
lines.push("")
|
|
286
|
+
|
|
287
|
+
lines.push("📋 Configuración recomendada para tu PC:")
|
|
288
|
+
lines.push(` Perfil: ${recommendation.profile}`)
|
|
289
|
+
lines.push(` Modo agentes: ${recommendation.agentMode}`)
|
|
290
|
+
lines.push(` Concurrencia: ${recommendation.maxConcurrency} agente(s) simultáneo(s)`)
|
|
291
|
+
lines.push(` Modelo: ${recommendation.recommendedModel}`)
|
|
292
|
+
lines.push(` Razón: ${recommendation.profileReason}`)
|
|
293
|
+
lines.push("")
|
|
294
|
+
|
|
295
|
+
if (!ollama.installed) {
|
|
296
|
+
lines.push("⚠️ IMPORTANTE: Ollama NO está instalado.")
|
|
297
|
+
lines.push(" Boost necesita Ollama para funcionar. Instálalo con:")
|
|
298
|
+
lines.push("")
|
|
299
|
+
lines.push(" curl -fsSL https://ollama.com/install.sh | sh")
|
|
300
|
+
lines.push("")
|
|
301
|
+
lines.push(" Luego descarga un modelo:")
|
|
302
|
+
lines.push(` ollama pull ${recommendation.recommendedModel}`)
|
|
303
|
+
lines.push("")
|
|
304
|
+
lines.push(" Después, ejecuta: opl boost setup")
|
|
305
|
+
lines.push("")
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
lines.push("✅ Configuración guardada en prompt-lang.json")
|
|
309
|
+
lines.push(" Puedes re-ejecutar: opl boost setup --re-detect")
|
|
310
|
+
lines.push("")
|
|
311
|
+
|
|
312
|
+
return lines.join("\n")
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ──────────────────────────────────────────────
|
|
316
|
+
// Monitoreo en tiempo real
|
|
317
|
+
// ──────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Estado actual del sistema en tiempo real.
|
|
321
|
+
* A diferencia de detectHardware() (que es estático), esto se llama
|
|
322
|
+
* en cada solicitud de agente para decidir si ejecutar ahora o esperar.
|
|
323
|
+
*/
|
|
324
|
+
// Modelos de Ollama en RAM estimado (valores típicos)
|
|
325
|
+
const MODEL_RAM_ESTIMATE = {
|
|
326
|
+
"llama3.2:latest": 2.5, // GB
|
|
327
|
+
"qwen2.5-coder:7b": 5.5, // GB
|
|
328
|
+
"llama3:latest": 6.0, // GB
|
|
329
|
+
"deepseek-coder:7b": 5.0, // GB
|
|
330
|
+
"default": 4.0, // GB estimado genérico
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function getRuntimeStatus() {
|
|
334
|
+
const freeBytes = os.freemem()
|
|
335
|
+
const totalBytes = os.totalmem()
|
|
336
|
+
const freeGB = bytesToGB(freeBytes)
|
|
337
|
+
const totalGB = bytesToGB(totalBytes)
|
|
338
|
+
const usedGB = totalGB - freeGB
|
|
339
|
+
const usagePercent = Math.round((1 - freeBytes / totalBytes) * 100)
|
|
340
|
+
|
|
341
|
+
// Carga del sistema
|
|
342
|
+
const loadAvg = os.loadavg ? os.loadavg() : [0, 0, 0]
|
|
343
|
+
const cpuCount = os.cpus().length
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
timestamp: new Date().toISOString(),
|
|
347
|
+
ram: {
|
|
348
|
+
totalGB,
|
|
349
|
+
freeGB,
|
|
350
|
+
usedGB,
|
|
351
|
+
usagePercent,
|
|
352
|
+
isCritical: freeGB < 2,
|
|
353
|
+
isLow: freeGB < 4,
|
|
354
|
+
isHealthy: freeGB >= 6,
|
|
355
|
+
},
|
|
356
|
+
cpu: {
|
|
357
|
+
cores: cpuCount,
|
|
358
|
+
loadAvg1m: loadAvg[0]?.toFixed(1) || "0.0",
|
|
359
|
+
loadAvg5m: loadAvg[1]?.toFixed(1) || "0.0",
|
|
360
|
+
isLoaded: cpuCount > 0 && loadAvg[0] > cpuCount * 0.8,
|
|
361
|
+
},
|
|
362
|
+
safeToRunAgent: freeGB >= 4,
|
|
363
|
+
warning: null,
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Determina cuántos agentes pueden ejecutarse AHORA sin saturar el PC.
|
|
369
|
+
* Considera RAM libre actual + carga actual de CPU.
|
|
370
|
+
*
|
|
371
|
+
* @param {object} hardwareConfig - Config de hardware guardada (opcional)
|
|
372
|
+
* @returns {{ safe: boolean, maxAgents: number, mode: string, reason: string }}
|
|
373
|
+
*/
|
|
374
|
+
export function getSafeParallelism(hardwareConfig) {
|
|
375
|
+
const runtime = getRuntimeStatus()
|
|
376
|
+
|
|
377
|
+
// Reglas basadas en RAM libre ACTUAL
|
|
378
|
+
if (runtime.ram.isCritical) {
|
|
379
|
+
return {
|
|
380
|
+
safe: false,
|
|
381
|
+
maxAgents: 0,
|
|
382
|
+
mode: "blocked",
|
|
383
|
+
reason: `RAM crítica: ${runtime.ram.freeGB}GB libres. Libera memoria antes de ejecutar agentes.`,
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (runtime.ram.isLow) {
|
|
388
|
+
return {
|
|
389
|
+
safe: true,
|
|
390
|
+
maxAgents: 1,
|
|
391
|
+
mode: "queue",
|
|
392
|
+
reason: `RAM baja: ${runtime.ram.freeGB}GB libres. Solo 1 agente en cola.`,
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (runtime.cpu.isLoaded) {
|
|
397
|
+
return {
|
|
398
|
+
safe: true,
|
|
399
|
+
maxAgents: 1,
|
|
400
|
+
mode: "queue",
|
|
401
|
+
reason: `CPU cargada (load: ${runtime.cpu.loadAvg1m}). Reduciendo a 1 agente.`,
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// RAM saludable — usar configuración guardada
|
|
406
|
+
const configuredMax = hardwareConfig?.agentPool?.maxConcurrency || 2
|
|
407
|
+
|
|
408
|
+
if (runtime.ram.freeGB >= 16 && configuredMax >= 2) {
|
|
409
|
+
return {
|
|
410
|
+
safe: true,
|
|
411
|
+
maxAgents: Math.min(configuredMax, 3),
|
|
412
|
+
mode: "parallel",
|
|
413
|
+
reason: `RAM saludable: ${runtime.ram.freeGB}GB libres. Hasta ${Math.min(configuredMax, 3)} agentes en paralelo.`,
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (runtime.ram.freeGB >= 8) {
|
|
418
|
+
return {
|
|
419
|
+
safe: true,
|
|
420
|
+
maxAgents: Math.min(configuredMax, 2),
|
|
421
|
+
mode: "parallel",
|
|
422
|
+
reason: `RAM suficiente: ${runtime.ram.freeGB}GB libres. Hasta ${Math.min(configuredMax, 2)} agentes.`,
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
safe: true,
|
|
428
|
+
maxAgents: 1,
|
|
429
|
+
mode: "queue",
|
|
430
|
+
reason: `RAM moderada: ${runtime.ram.freeGB}GB libres. 1 agente por vez.`,
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Genera una advertencia si el sistema está muy cargado.
|
|
436
|
+
* @returns {string|null} Mensaje de advertencia o null si todo bien
|
|
437
|
+
*/
|
|
438
|
+
export function getRAMWarning() {
|
|
439
|
+
const runtime = getRuntimeStatus()
|
|
440
|
+
|
|
441
|
+
if (runtime.ram.isCritical) {
|
|
442
|
+
return (
|
|
443
|
+
`⚠️ RAM CRÍTICA: Solo ${runtime.ram.freeGB}GB libres de ${runtime.ram.totalGB}GB (${runtime.ram.usagePercent}% usado).\n` +
|
|
444
|
+
` Los agentes Boost NO se ejecutarán hasta que liberes memoria.\n` +
|
|
445
|
+
` → Cierra navegadores, Docker, o programas pesados.\n` +
|
|
446
|
+
` → Luego ejecuta: opl boost check`
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (runtime.ram.isLow && runtime.cpu.isLoaded) {
|
|
451
|
+
return (
|
|
452
|
+
`⚠️ RAM baja (${runtime.ram.freeGB}GB libres) y CPU cargada (load: ${runtime.cpu.loadAvg1m}).\n` +
|
|
453
|
+
` Los agentes se ejecutarán en MODO COLA (1 por vez) para no saturar.\n` +
|
|
454
|
+
` Será más lento, pero tu PC se mantendrá usable.`
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (runtime.ram.isLow) {
|
|
459
|
+
return (
|
|
460
|
+
`ℹ️ RAM baja: ${runtime.ram.freeGB}GB libres de ${runtime.ram.totalGB}GB.\n` +
|
|
461
|
+
` Modo cola activado. Si notas lentitud, cierra otras aplicaciones.`
|
|
462
|
+
)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (runtime.cpu.isLoaded) {
|
|
466
|
+
return (
|
|
467
|
+
`ℹ️ CPU cargada (load: ${runtime.cpu.loadAvg1m}/${runtime.cpu.cores} núcleos).\n` +
|
|
468
|
+
` Los agentes se ralentizarán automáticamente.`
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return null
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Calcula cuánta RAM消耗rá un modelo específico en Ollama.
|
|
477
|
+
* @param {string} modelName - Nombre del modelo
|
|
478
|
+
* @returns {number} RAM estimada en GB
|
|
479
|
+
*/
|
|
480
|
+
export function estimateModelRAM(modelName) {
|
|
481
|
+
const name = modelName?.toLowerCase() || ""
|
|
482
|
+
for (const [key, gb] of Object.entries(MODEL_RAM_ESTIMATE)) {
|
|
483
|
+
if (name.includes(key.replace(":latest", ""))) return gb
|
|
484
|
+
}
|
|
485
|
+
return MODEL_RAM_ESTIMATE.default
|
|
486
|
+
}
|