@saulwade/swl-ses 1.3.3 → 1.3.5
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/CLAUDE.md +1 -1
- package/README.md +1 -1
- package/bin/swl-mcp-server.js +187 -187
- package/bin/swl-ses.js +4 -62
- package/comandos/swl/.evolved.json +22 -22
- package/comandos/swl/adoptar-proyecto.md +207 -207
- package/comandos/swl/contribuir.md +233 -233
- package/habilidades/backend-production-resilience/SKILL.md +288 -288
- package/habilidades/benchmark-memoria/SKILL.md +186 -186
- package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
- package/habilidades/doubt-driven-review/SKILL.md +171 -171
- package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
- package/habilidades/eval-framework/SKILL.md +212 -212
- package/habilidades/extractor-de-aprendizajes/SKILL.md +321 -321
- package/habilidades/harness-claude-code/SKILL.md +299 -299
- package/habilidades/infra-github-actions/SKILL.md +166 -166
- package/habilidades/legacy-code-rescue/SKILL.md +267 -267
- package/habilidades/manejo-errores/.evolved.json +8 -8
- package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
- package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
- package/habilidades/patrones-python/SKILL.md +229 -229
- package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
- package/habilidades/planear-fase/SKILL.md +319 -319
- package/habilidades/release-semver/.evolved.json +8 -8
- package/habilidades/swl-claudemd/SKILL.md +220 -220
- package/habilidades/testing-python/SKILL.md +340 -340
- package/hooks/claudemd-bloat-detector.js +161 -161
- package/hooks/extraccion-aprendizajes.js +43 -12
- package/hooks/lib/agent-routing.js +107 -107
- package/hooks/lib/auto-consolidator.js +335 -335
- package/hooks/lib/error-classifier.js +308 -308
- package/hooks/lib/merkle-audit.js +96 -96
- package/hooks/lib/provenance-tracker.js +191 -191
- package/hooks/lib/rate-limit-tracker.js +253 -253
- package/hooks/lib/resource-quota.js +122 -122
- package/hooks/lib/retry-jitter.js +165 -165
- package/hooks/lib/skill-auditor.js +588 -588
- package/hooks/lib/sync-status.js +228 -228
- package/hooks/lib/taint-tracker.js +107 -107
- package/hooks/lib/text-similarity.js +241 -241
- package/hooks/lib/toon-compressor.js +245 -245
- package/hooks/registro-turnos.js +209 -209
- package/hooks/sugerir-regenerar-inventario.js +170 -170
- package/hooks/validar-formato-post-subagente.js +140 -140
- package/hooks/validar-memoria-hook.js +218 -218
- package/instintos/prompt-appendices.yaml +57 -57
- package/manifiestos/agent-output-schemas.json +57 -57
- package/manifiestos/skills-lock.json +27 -27
- package/package.json +1 -1
- package/plantillas/auditor-veto-template.md +105 -105
- package/plantillas/github-workflows/README.md +47 -47
- package/plantillas/github-workflows/release-please.yml +44 -44
- package/plantillas/github-workflows/swl-ci.yml +107 -107
- package/plantillas/github-workflows/swl-security.yml +51 -51
- package/plugin.json +1 -1
- package/reglas/analisis-previo-tareas-grandes.md +172 -172
- package/reglas/arreglar-al-detectar.md +147 -147
- package/reglas/fragmentos-compartidos.md +152 -152
- package/reglas/harness-claude-code.md +213 -213
- package/reglas/usar-context7.md +226 -226
- package/schemas/diary-entry.schema.json +80 -80
- package/scripts/benchmark-memoria.js +167 -167
- package/scripts/configurar-branch-protection.js +418 -418
- package/scripts/detectar-aprendizajes-duplicados.js +151 -151
- package/scripts/doctor.js +77 -3
- package/scripts/field-report.js +199 -199
- package/scripts/generar-checklists-consolidados.js +273 -273
- package/scripts/generar-inventario.js +420 -420
- package/scripts/generar-matriz-lenguajes.js +271 -271
- package/scripts/instalador.js +38 -1
- package/scripts/lib/artefactos-python.js +43 -43
- package/scripts/lib/benchmark-metrics.js +160 -160
- package/scripts/lib/budget-enforcer.js +252 -252
- package/scripts/lib/configurar-ci.js +380 -380
- package/scripts/lib/contadores-inventario.js +217 -217
- package/scripts/lib/detectar-stack-detallado.js +307 -307
- package/scripts/lib/diary-entry.js +234 -234
- package/scripts/lib/eval-metrics-store.js +218 -218
- package/scripts/lib/eval-quality.js +171 -171
- package/scripts/lib/eval-schemas.js +144 -144
- package/scripts/lib/eval-self-correct.js +106 -106
- package/scripts/lib/eval-validator.js +185 -185
- package/scripts/lib/jaccard-similarity.js +98 -98
- package/scripts/lib/longmemeval-runner.js +125 -125
- package/scripts/lib/npm-version.js +261 -261
- package/scripts/lib/paquetes-conocidos.js +50 -50
- package/scripts/lib/parsear-opciones.js +136 -0
- package/scripts/lib/prompt-builder.js +264 -264
- package/scripts/lib/rrf-fusion.js +175 -175
- package/scripts/lib/scoring-instintos.js +277 -277
- package/scripts/lib/semantic-search.js +252 -252
- package/scripts/lib/transformadores/claude.js +200 -200
- package/scripts/limpiar-artefactos-python.js +131 -131
- package/scripts/mcp-server/README.md +128 -128
- package/scripts/mcp-server/handlers.js +206 -206
- package/scripts/migrar-csv-a-array.js +168 -168
- package/scripts/migrar-fase-dominio.js +201 -201
- package/scripts/publicar.js +511 -511
- package/scripts/run-eval.js +141 -141
- package/scripts/validar-manifest.js +195 -195
- package/scripts/validar-userland-vacio.js +110 -110
- package/scripts/verificar-release.js +5 -1
|
@@ -1,252 +1,252 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* budget-enforcer.js
|
|
5
|
-
*
|
|
6
|
-
* Control de presupuesto con thresholds escalonados, backpressure y
|
|
7
|
-
* idempotency keys. Patrón adaptado del BudgetManager de Shannon
|
|
8
|
-
* (`temp/Shannon-main/go/orchestrator/internal/budget/manager.go`),
|
|
9
|
-
* portado a Node.js zero-deps.
|
|
10
|
-
*
|
|
11
|
-
* Diferencias con `hooks/tracking-costos.js`:
|
|
12
|
-
* - tracking-costos: registra consumo y emite warnings simples (80%, 100%).
|
|
13
|
-
* - budget-enforcer: 4 niveles (warning/approval/hard/backpressure),
|
|
14
|
-
* idempotency keys para reintentos, decisiones puras (no I/O).
|
|
15
|
-
*
|
|
16
|
-
* El módulo es PURO. No escribe a disco. El consumidor decide qué hacer
|
|
17
|
-
* con el resultado (alertar, pausar, bloquear, retrasar).
|
|
18
|
-
*
|
|
19
|
-
* Modelo de niveles:
|
|
20
|
-
* - PASS : por debajo de warning. Continúa sin restricción.
|
|
21
|
-
* - WARNING : ≥ warning_pct. Emite alerta, continúa.
|
|
22
|
-
* - APPROVAL : ≥ approval_pct. Requiere confirmación humana antes de seguir.
|
|
23
|
-
* - BACKPRESSURE : ≥ backpressure_pct. Continúa pero con delay configurable.
|
|
24
|
-
* - HARD_LIMIT : ≥ hard_pct. Detiene la operación; sin override.
|
|
25
|
-
*
|
|
26
|
-
* Defaults razonables (modificables vía opts):
|
|
27
|
-
* warning_pct = 0.70 (70%)
|
|
28
|
-
* approval_pct = 0.85 (85%)
|
|
29
|
-
* backpressure_pct = 0.90 (90%)
|
|
30
|
-
* hard_pct = 1.00 (100%)
|
|
31
|
-
* backpressure_delay_ms = 2000
|
|
32
|
-
*
|
|
33
|
-
* Idempotency:
|
|
34
|
-
* recordUsage acepta una idempotency_key. Si ya se contabilizó esa key,
|
|
35
|
-
* devuelve el estado SIN re-incrementar. Permite reintentos seguros.
|
|
36
|
-
*
|
|
37
|
-
* @module scripts/lib/budget-enforcer
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
// ── constantes ────────────────────────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
const DEFAULTS = Object.freeze({
|
|
43
|
-
warning_pct: 0.70,
|
|
44
|
-
approval_pct: 0.85,
|
|
45
|
-
backpressure_pct: 0.90,
|
|
46
|
-
hard_pct: 1.00,
|
|
47
|
-
backpressure_delay_ms: 2000,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const LEVEL = Object.freeze({
|
|
51
|
-
PASS: 'pass',
|
|
52
|
-
WARNING: 'warning',
|
|
53
|
-
APPROVAL: 'approval',
|
|
54
|
-
BACKPRESSURE: 'backpressure',
|
|
55
|
-
HARD_LIMIT: 'hard_limit',
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
59
|
-
|
|
60
|
-
function clamp(n, min, max) {
|
|
61
|
-
if (Number.isNaN(n)) return min;
|
|
62
|
-
return Math.max(min, Math.min(max, n));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function mergeOpts(opts) {
|
|
66
|
-
return { ...DEFAULTS, ...(opts || {}) };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ── API pública ───────────────────────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Crea un nuevo estado de presupuesto.
|
|
73
|
-
*
|
|
74
|
-
* @param {object} params
|
|
75
|
-
* @param {number} params.maxUsd - Presupuesto máximo en USD
|
|
76
|
-
* @param {number} params.maxTokens - Presupuesto máximo en tokens
|
|
77
|
-
* @returns {object} estado inicial
|
|
78
|
-
*/
|
|
79
|
-
function createBudget({ maxUsd = 10.0, maxTokens = 500_000 } = {}) {
|
|
80
|
-
return {
|
|
81
|
-
maxUsd,
|
|
82
|
-
maxTokens,
|
|
83
|
-
usedUsd: 0,
|
|
84
|
-
usedTokens: 0,
|
|
85
|
-
requestCount: 0,
|
|
86
|
-
idempotency: {}, // key → { usd, tokens, recordedAt }
|
|
87
|
-
createdAt: new Date().toISOString(),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Registra consumo de presupuesto. Pure: devuelve nuevo estado.
|
|
93
|
-
*
|
|
94
|
-
* @param {object} budget - estado actual
|
|
95
|
-
* @param {object} usage - consumo a registrar
|
|
96
|
-
* @param {number} usage.usd
|
|
97
|
-
* @param {number} usage.tokens
|
|
98
|
-
* @param {string} [usage.idempotencyKey] - si presente, evita doble-conteo
|
|
99
|
-
* @returns {{ budget: object, deduplicated: boolean }}
|
|
100
|
-
*/
|
|
101
|
-
function recordUsage(budget, usage) {
|
|
102
|
-
const next = { ...budget, idempotency: { ...budget.idempotency } };
|
|
103
|
-
const usd = Number(usage.usd) || 0;
|
|
104
|
-
const tokens = Number(usage.tokens) || 0;
|
|
105
|
-
const key = usage.idempotencyKey;
|
|
106
|
-
|
|
107
|
-
if (key && next.idempotency[key]) {
|
|
108
|
-
// Ya contabilizado, retornar sin cambios
|
|
109
|
-
return { budget: next, deduplicated: true };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
next.usedUsd = next.usedUsd + usd;
|
|
113
|
-
next.usedTokens = next.usedTokens + tokens;
|
|
114
|
-
next.requestCount = next.requestCount + 1;
|
|
115
|
-
|
|
116
|
-
if (key) {
|
|
117
|
-
next.idempotency[key] = {
|
|
118
|
-
usd,
|
|
119
|
-
tokens,
|
|
120
|
-
recordedAt: new Date().toISOString(),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return { budget: next, deduplicated: false };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Calcula la fracción del presupuesto consumida.
|
|
129
|
-
* Devuelve max(usd_pct, tokens_pct) — el más cercano al límite manda.
|
|
130
|
-
*
|
|
131
|
-
* @param {object} budget
|
|
132
|
-
* @returns {number} en [0, ∞) — sí puede pasar 1.0 si hay overshoot
|
|
133
|
-
*/
|
|
134
|
-
function consumptionRatio(budget) {
|
|
135
|
-
const usdPct = budget.maxUsd > 0 ? budget.usedUsd / budget.maxUsd : 0;
|
|
136
|
-
const tokensPct = budget.maxTokens > 0 ? budget.usedTokens / budget.maxTokens : 0;
|
|
137
|
-
return Math.max(usdPct, tokensPct);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Determina el nivel de threshold actual.
|
|
142
|
-
*
|
|
143
|
-
* @param {object} budget
|
|
144
|
-
* @param {object} [opts] - thresholds custom
|
|
145
|
-
* @returns {string} LEVEL.*
|
|
146
|
-
*/
|
|
147
|
-
function currentLevel(budget, opts) {
|
|
148
|
-
const cfg = mergeOpts(opts);
|
|
149
|
-
const ratio = consumptionRatio(budget);
|
|
150
|
-
|
|
151
|
-
if (ratio >= cfg.hard_pct) return LEVEL.HARD_LIMIT;
|
|
152
|
-
if (ratio >= cfg.backpressure_pct) return LEVEL.BACKPRESSURE;
|
|
153
|
-
if (ratio >= cfg.approval_pct) return LEVEL.APPROVAL;
|
|
154
|
-
if (ratio >= cfg.warning_pct) return LEVEL.WARNING;
|
|
155
|
-
return LEVEL.PASS;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Decisión combinada: nivel + acción recomendada + delay si aplica.
|
|
160
|
-
*
|
|
161
|
-
* @param {object} budget
|
|
162
|
-
* @param {object} [opts]
|
|
163
|
-
* @returns {object} {
|
|
164
|
-
* level, ratio, allow, requiresApproval, delayMs, reason
|
|
165
|
-
* }
|
|
166
|
-
*/
|
|
167
|
-
function decide(budget, opts) {
|
|
168
|
-
const cfg = mergeOpts(opts);
|
|
169
|
-
const ratio = consumptionRatio(budget);
|
|
170
|
-
const level = currentLevel(budget, cfg);
|
|
171
|
-
|
|
172
|
-
const decision = {
|
|
173
|
-
level,
|
|
174
|
-
ratio: Math.round(ratio * 1000) / 1000,
|
|
175
|
-
allow: true,
|
|
176
|
-
requiresApproval: false,
|
|
177
|
-
delayMs: 0,
|
|
178
|
-
reason: null,
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
switch (level) {
|
|
182
|
-
case LEVEL.HARD_LIMIT:
|
|
183
|
-
decision.allow = false;
|
|
184
|
-
decision.reason = `Presupuesto agotado (${(ratio * 100).toFixed(1)}% del máximo). Hard limit alcanzado.`;
|
|
185
|
-
break;
|
|
186
|
-
case LEVEL.BACKPRESSURE:
|
|
187
|
-
decision.delayMs = cfg.backpressure_delay_ms;
|
|
188
|
-
decision.reason = `Cerca del límite (${(ratio * 100).toFixed(1)}%). Aplicando backpressure de ${cfg.backpressure_delay_ms}ms.`;
|
|
189
|
-
break;
|
|
190
|
-
case LEVEL.APPROVAL:
|
|
191
|
-
decision.requiresApproval = true;
|
|
192
|
-
decision.reason = `Presupuesto al ${(ratio * 100).toFixed(1)}%. Requiere confirmación humana antes de continuar.`;
|
|
193
|
-
break;
|
|
194
|
-
case LEVEL.WARNING:
|
|
195
|
-
decision.reason = `Aviso: presupuesto al ${(ratio * 100).toFixed(1)}% del máximo.`;
|
|
196
|
-
break;
|
|
197
|
-
case LEVEL.PASS:
|
|
198
|
-
default:
|
|
199
|
-
break;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return decision;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Estima si una operación futura cabe en el presupuesto.
|
|
207
|
-
* Útil para decidir antes de invocar un agente costoso.
|
|
208
|
-
*
|
|
209
|
-
* @param {object} budget
|
|
210
|
-
* @param {object} estimate
|
|
211
|
-
* @param {number} estimate.usd
|
|
212
|
-
* @param {number} estimate.tokens
|
|
213
|
-
* @param {object} [opts]
|
|
214
|
-
* @returns {object} mismo formato que decide() — proyecta el estado post-operación
|
|
215
|
-
*/
|
|
216
|
-
function projectDecision(budget, estimate, opts) {
|
|
217
|
-
const projected = {
|
|
218
|
-
...budget,
|
|
219
|
-
usedUsd: budget.usedUsd + (Number(estimate.usd) || 0),
|
|
220
|
-
usedTokens: budget.usedTokens + (Number(estimate.tokens) || 0),
|
|
221
|
-
};
|
|
222
|
-
return decide(projected, opts);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Resetea el contador de uso manteniendo límites e idempotency cache.
|
|
227
|
-
* Útil para nuevos ciclos de facturación.
|
|
228
|
-
*/
|
|
229
|
-
function resetUsage(budget) {
|
|
230
|
-
return {
|
|
231
|
-
...budget,
|
|
232
|
-
usedUsd: 0,
|
|
233
|
-
usedTokens: 0,
|
|
234
|
-
requestCount: 0,
|
|
235
|
-
idempotency: {},
|
|
236
|
-
resetAt: new Date().toISOString(),
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// ── exports ───────────────────────────────────────────────────────────────────
|
|
241
|
-
|
|
242
|
-
module.exports = {
|
|
243
|
-
createBudget,
|
|
244
|
-
recordUsage,
|
|
245
|
-
consumptionRatio,
|
|
246
|
-
currentLevel,
|
|
247
|
-
decide,
|
|
248
|
-
projectDecision,
|
|
249
|
-
resetUsage,
|
|
250
|
-
LEVEL,
|
|
251
|
-
DEFAULTS,
|
|
252
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* budget-enforcer.js
|
|
5
|
+
*
|
|
6
|
+
* Control de presupuesto con thresholds escalonados, backpressure y
|
|
7
|
+
* idempotency keys. Patrón adaptado del BudgetManager de Shannon
|
|
8
|
+
* (`temp/Shannon-main/go/orchestrator/internal/budget/manager.go`),
|
|
9
|
+
* portado a Node.js zero-deps.
|
|
10
|
+
*
|
|
11
|
+
* Diferencias con `hooks/tracking-costos.js`:
|
|
12
|
+
* - tracking-costos: registra consumo y emite warnings simples (80%, 100%).
|
|
13
|
+
* - budget-enforcer: 4 niveles (warning/approval/hard/backpressure),
|
|
14
|
+
* idempotency keys para reintentos, decisiones puras (no I/O).
|
|
15
|
+
*
|
|
16
|
+
* El módulo es PURO. No escribe a disco. El consumidor decide qué hacer
|
|
17
|
+
* con el resultado (alertar, pausar, bloquear, retrasar).
|
|
18
|
+
*
|
|
19
|
+
* Modelo de niveles:
|
|
20
|
+
* - PASS : por debajo de warning. Continúa sin restricción.
|
|
21
|
+
* - WARNING : ≥ warning_pct. Emite alerta, continúa.
|
|
22
|
+
* - APPROVAL : ≥ approval_pct. Requiere confirmación humana antes de seguir.
|
|
23
|
+
* - BACKPRESSURE : ≥ backpressure_pct. Continúa pero con delay configurable.
|
|
24
|
+
* - HARD_LIMIT : ≥ hard_pct. Detiene la operación; sin override.
|
|
25
|
+
*
|
|
26
|
+
* Defaults razonables (modificables vía opts):
|
|
27
|
+
* warning_pct = 0.70 (70%)
|
|
28
|
+
* approval_pct = 0.85 (85%)
|
|
29
|
+
* backpressure_pct = 0.90 (90%)
|
|
30
|
+
* hard_pct = 1.00 (100%)
|
|
31
|
+
* backpressure_delay_ms = 2000
|
|
32
|
+
*
|
|
33
|
+
* Idempotency:
|
|
34
|
+
* recordUsage acepta una idempotency_key. Si ya se contabilizó esa key,
|
|
35
|
+
* devuelve el estado SIN re-incrementar. Permite reintentos seguros.
|
|
36
|
+
*
|
|
37
|
+
* @module scripts/lib/budget-enforcer
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
// ── constantes ────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const DEFAULTS = Object.freeze({
|
|
43
|
+
warning_pct: 0.70,
|
|
44
|
+
approval_pct: 0.85,
|
|
45
|
+
backpressure_pct: 0.90,
|
|
46
|
+
hard_pct: 1.00,
|
|
47
|
+
backpressure_delay_ms: 2000,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const LEVEL = Object.freeze({
|
|
51
|
+
PASS: 'pass',
|
|
52
|
+
WARNING: 'warning',
|
|
53
|
+
APPROVAL: 'approval',
|
|
54
|
+
BACKPRESSURE: 'backpressure',
|
|
55
|
+
HARD_LIMIT: 'hard_limit',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
function clamp(n, min, max) {
|
|
61
|
+
if (Number.isNaN(n)) return min;
|
|
62
|
+
return Math.max(min, Math.min(max, n));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function mergeOpts(opts) {
|
|
66
|
+
return { ...DEFAULTS, ...(opts || {}) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── API pública ───────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Crea un nuevo estado de presupuesto.
|
|
73
|
+
*
|
|
74
|
+
* @param {object} params
|
|
75
|
+
* @param {number} params.maxUsd - Presupuesto máximo en USD
|
|
76
|
+
* @param {number} params.maxTokens - Presupuesto máximo en tokens
|
|
77
|
+
* @returns {object} estado inicial
|
|
78
|
+
*/
|
|
79
|
+
function createBudget({ maxUsd = 10.0, maxTokens = 500_000 } = {}) {
|
|
80
|
+
return {
|
|
81
|
+
maxUsd,
|
|
82
|
+
maxTokens,
|
|
83
|
+
usedUsd: 0,
|
|
84
|
+
usedTokens: 0,
|
|
85
|
+
requestCount: 0,
|
|
86
|
+
idempotency: {}, // key → { usd, tokens, recordedAt }
|
|
87
|
+
createdAt: new Date().toISOString(),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Registra consumo de presupuesto. Pure: devuelve nuevo estado.
|
|
93
|
+
*
|
|
94
|
+
* @param {object} budget - estado actual
|
|
95
|
+
* @param {object} usage - consumo a registrar
|
|
96
|
+
* @param {number} usage.usd
|
|
97
|
+
* @param {number} usage.tokens
|
|
98
|
+
* @param {string} [usage.idempotencyKey] - si presente, evita doble-conteo
|
|
99
|
+
* @returns {{ budget: object, deduplicated: boolean }}
|
|
100
|
+
*/
|
|
101
|
+
function recordUsage(budget, usage) {
|
|
102
|
+
const next = { ...budget, idempotency: { ...budget.idempotency } };
|
|
103
|
+
const usd = Number(usage.usd) || 0;
|
|
104
|
+
const tokens = Number(usage.tokens) || 0;
|
|
105
|
+
const key = usage.idempotencyKey;
|
|
106
|
+
|
|
107
|
+
if (key && next.idempotency[key]) {
|
|
108
|
+
// Ya contabilizado, retornar sin cambios
|
|
109
|
+
return { budget: next, deduplicated: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
next.usedUsd = next.usedUsd + usd;
|
|
113
|
+
next.usedTokens = next.usedTokens + tokens;
|
|
114
|
+
next.requestCount = next.requestCount + 1;
|
|
115
|
+
|
|
116
|
+
if (key) {
|
|
117
|
+
next.idempotency[key] = {
|
|
118
|
+
usd,
|
|
119
|
+
tokens,
|
|
120
|
+
recordedAt: new Date().toISOString(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { budget: next, deduplicated: false };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Calcula la fracción del presupuesto consumida.
|
|
129
|
+
* Devuelve max(usd_pct, tokens_pct) — el más cercano al límite manda.
|
|
130
|
+
*
|
|
131
|
+
* @param {object} budget
|
|
132
|
+
* @returns {number} en [0, ∞) — sí puede pasar 1.0 si hay overshoot
|
|
133
|
+
*/
|
|
134
|
+
function consumptionRatio(budget) {
|
|
135
|
+
const usdPct = budget.maxUsd > 0 ? budget.usedUsd / budget.maxUsd : 0;
|
|
136
|
+
const tokensPct = budget.maxTokens > 0 ? budget.usedTokens / budget.maxTokens : 0;
|
|
137
|
+
return Math.max(usdPct, tokensPct);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Determina el nivel de threshold actual.
|
|
142
|
+
*
|
|
143
|
+
* @param {object} budget
|
|
144
|
+
* @param {object} [opts] - thresholds custom
|
|
145
|
+
* @returns {string} LEVEL.*
|
|
146
|
+
*/
|
|
147
|
+
function currentLevel(budget, opts) {
|
|
148
|
+
const cfg = mergeOpts(opts);
|
|
149
|
+
const ratio = consumptionRatio(budget);
|
|
150
|
+
|
|
151
|
+
if (ratio >= cfg.hard_pct) return LEVEL.HARD_LIMIT;
|
|
152
|
+
if (ratio >= cfg.backpressure_pct) return LEVEL.BACKPRESSURE;
|
|
153
|
+
if (ratio >= cfg.approval_pct) return LEVEL.APPROVAL;
|
|
154
|
+
if (ratio >= cfg.warning_pct) return LEVEL.WARNING;
|
|
155
|
+
return LEVEL.PASS;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Decisión combinada: nivel + acción recomendada + delay si aplica.
|
|
160
|
+
*
|
|
161
|
+
* @param {object} budget
|
|
162
|
+
* @param {object} [opts]
|
|
163
|
+
* @returns {object} {
|
|
164
|
+
* level, ratio, allow, requiresApproval, delayMs, reason
|
|
165
|
+
* }
|
|
166
|
+
*/
|
|
167
|
+
function decide(budget, opts) {
|
|
168
|
+
const cfg = mergeOpts(opts);
|
|
169
|
+
const ratio = consumptionRatio(budget);
|
|
170
|
+
const level = currentLevel(budget, cfg);
|
|
171
|
+
|
|
172
|
+
const decision = {
|
|
173
|
+
level,
|
|
174
|
+
ratio: Math.round(ratio * 1000) / 1000,
|
|
175
|
+
allow: true,
|
|
176
|
+
requiresApproval: false,
|
|
177
|
+
delayMs: 0,
|
|
178
|
+
reason: null,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
switch (level) {
|
|
182
|
+
case LEVEL.HARD_LIMIT:
|
|
183
|
+
decision.allow = false;
|
|
184
|
+
decision.reason = `Presupuesto agotado (${(ratio * 100).toFixed(1)}% del máximo). Hard limit alcanzado.`;
|
|
185
|
+
break;
|
|
186
|
+
case LEVEL.BACKPRESSURE:
|
|
187
|
+
decision.delayMs = cfg.backpressure_delay_ms;
|
|
188
|
+
decision.reason = `Cerca del límite (${(ratio * 100).toFixed(1)}%). Aplicando backpressure de ${cfg.backpressure_delay_ms}ms.`;
|
|
189
|
+
break;
|
|
190
|
+
case LEVEL.APPROVAL:
|
|
191
|
+
decision.requiresApproval = true;
|
|
192
|
+
decision.reason = `Presupuesto al ${(ratio * 100).toFixed(1)}%. Requiere confirmación humana antes de continuar.`;
|
|
193
|
+
break;
|
|
194
|
+
case LEVEL.WARNING:
|
|
195
|
+
decision.reason = `Aviso: presupuesto al ${(ratio * 100).toFixed(1)}% del máximo.`;
|
|
196
|
+
break;
|
|
197
|
+
case LEVEL.PASS:
|
|
198
|
+
default:
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return decision;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Estima si una operación futura cabe en el presupuesto.
|
|
207
|
+
* Útil para decidir antes de invocar un agente costoso.
|
|
208
|
+
*
|
|
209
|
+
* @param {object} budget
|
|
210
|
+
* @param {object} estimate
|
|
211
|
+
* @param {number} estimate.usd
|
|
212
|
+
* @param {number} estimate.tokens
|
|
213
|
+
* @param {object} [opts]
|
|
214
|
+
* @returns {object} mismo formato que decide() — proyecta el estado post-operación
|
|
215
|
+
*/
|
|
216
|
+
function projectDecision(budget, estimate, opts) {
|
|
217
|
+
const projected = {
|
|
218
|
+
...budget,
|
|
219
|
+
usedUsd: budget.usedUsd + (Number(estimate.usd) || 0),
|
|
220
|
+
usedTokens: budget.usedTokens + (Number(estimate.tokens) || 0),
|
|
221
|
+
};
|
|
222
|
+
return decide(projected, opts);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Resetea el contador de uso manteniendo límites e idempotency cache.
|
|
227
|
+
* Útil para nuevos ciclos de facturación.
|
|
228
|
+
*/
|
|
229
|
+
function resetUsage(budget) {
|
|
230
|
+
return {
|
|
231
|
+
...budget,
|
|
232
|
+
usedUsd: 0,
|
|
233
|
+
usedTokens: 0,
|
|
234
|
+
requestCount: 0,
|
|
235
|
+
idempotency: {},
|
|
236
|
+
resetAt: new Date().toISOString(),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── exports ───────────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
module.exports = {
|
|
243
|
+
createBudget,
|
|
244
|
+
recordUsage,
|
|
245
|
+
consumptionRatio,
|
|
246
|
+
currentLevel,
|
|
247
|
+
decide,
|
|
248
|
+
projectDecision,
|
|
249
|
+
resetUsage,
|
|
250
|
+
LEVEL,
|
|
251
|
+
DEFAULTS,
|
|
252
|
+
};
|