chocolatito-code 1.0.0 → 1.1.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/README.md +89 -0
- package/dist/agent/compaction.d.ts +65 -0
- package/dist/agent/compaction.js +134 -0
- package/dist/agent/context.d.ts +3 -0
- package/dist/agent/context.js +35 -0
- package/dist/agent/loop.d.ts +91 -2
- package/dist/agent/loop.js +536 -78
- package/dist/agent/loopDetector.d.ts +57 -0
- package/dist/agent/loopDetector.js +0 -0
- package/dist/agent/mentions.d.ts +13 -0
- package/dist/agent/mentions.js +49 -0
- package/dist/agent/repoMap.d.ts +117 -0
- package/dist/agent/repoMap.js +560 -0
- package/dist/agent/result.d.ts +64 -0
- package/dist/agent/result.js +48 -0
- package/dist/agent/retry.d.ts +35 -0
- package/dist/agent/retry.js +123 -4
- package/dist/agent/subagent.js +62 -15
- package/dist/agent/syntaxValidator.js +288 -52
- package/dist/agent/toolGate.d.ts +58 -0
- package/dist/agent/toolGate.js +111 -0
- package/dist/agent/tracker.js +4 -7
- package/dist/agent/undoManager.d.ts +44 -11
- package/dist/agent/undoManager.js +95 -24
- package/dist/agent/usageMeter.d.ts +56 -0
- package/dist/agent/usageMeter.js +51 -0
- package/dist/agent/verifier.d.ts +49 -0
- package/dist/agent/verifier.js +158 -0
- package/dist/config/constants.js +14 -0
- package/dist/config/engine.d.ts +19 -0
- package/dist/config/engine.js +35 -3
- package/dist/config/license.d.ts +5 -0
- package/dist/config/license.js +107 -9
- package/dist/config/limits.d.ts +28 -0
- package/dist/config/limits.js +51 -0
- package/dist/config/permissions.d.ts +76 -1
- package/dist/config/permissions.js +379 -23
- package/dist/config/quota.js +24 -1
- package/dist/config/updater.d.ts +68 -0
- package/dist/config/updater.js +215 -0
- package/dist/hooks/manager.js +159 -14
- package/dist/index.js +222 -34
- package/dist/mcp/client.d.ts +22 -0
- package/dist/mcp/client.js +93 -11
- package/dist/prompts/systemPrompt.js +12 -1
- package/dist/sessions/manager.d.ts +12 -0
- package/dist/sessions/manager.js +33 -5
- package/dist/sessions/resume.d.ts +36 -0
- package/dist/sessions/resume.js +43 -0
- package/dist/skills/manager.d.ts +1 -0
- package/dist/skills/manager.js +9 -0
- package/dist/tools/binary.d.ts +46 -0
- package/dist/tools/binary.js +100 -0
- package/dist/tools/browserCdp.js +13 -1
- package/dist/tools/browserExtension.d.ts +5 -0
- package/dist/tools/browserExtension.js +202 -18
- package/dist/tools/browserSession.d.ts +23 -0
- package/dist/tools/browserSession.js +28 -0
- package/dist/tools/computerUse.js +33 -1
- package/dist/tools/createImage.d.ts +11 -0
- package/dist/tools/createImage.js +13 -1
- package/dist/tools/definitions.js +9 -3
- package/dist/tools/editDiagnosis.d.ts +54 -0
- package/dist/tools/editDiagnosis.js +142 -0
- package/dist/tools/editFile.d.ts +1 -0
- package/dist/tools/editFile.js +118 -62
- package/dist/tools/findFiles.d.ts +21 -0
- package/dist/tools/findFiles.js +75 -6
- package/dist/tools/getSystemInfo.js +15 -7
- package/dist/tools/gitTools.d.ts +24 -0
- package/dist/tools/gitTools.js +87 -26
- package/dist/tools/moveCopyFile.d.ts +18 -1
- package/dist/tools/moveCopyFile.js +74 -4
- package/dist/tools/patchCascade.d.ts +57 -0
- package/dist/tools/patchCascade.js +338 -0
- package/dist/tools/runCommand.js +213 -25
- package/dist/tools/runner.js +57 -4
- package/dist/tools/toolDefsComputer.js +12 -2
- package/dist/tools/truncator.d.ts +27 -1
- package/dist/tools/truncator.js +117 -11
- package/dist/tools/viewFile.js +15 -11
- package/dist/tools/visionBridge.js +7 -0
- package/dist/tools/webSearch.d.ts +21 -0
- package/dist/tools/webSearch.js +125 -14
- package/dist/tools/writeFile.js +18 -1
- package/dist/ui/interrupt.d.ts +4 -1
- package/dist/ui/interrupt.js +56 -5
- package/dist/ui/keyboardGuard.d.ts +55 -0
- package/dist/ui/keyboardGuard.js +65 -0
- package/dist/ui/loopPrompt.d.ts +17 -0
- package/dist/ui/loopPrompt.js +59 -0
- package/dist/ui/permissionPrompt.js +84 -2
- package/dist/ui/prompt.d.ts +35 -0
- package/dist/ui/prompt.js +345 -125
- package/dist/ui/reasoningStream.d.ts +8 -0
- package/dist/ui/reasoningStream.js +99 -7
- package/extension/README.md +95 -74
- package/extension/background.js +598 -178
- package/extension/content.js +92 -9
- package/extension/manifest.json +2 -1
- package/extension/popup.js +28 -12
- package/package.json +1 -1
package/dist/agent/loop.js
CHANGED
|
@@ -5,20 +5,79 @@ import { executeToolCall } from "../tools/runner.js";
|
|
|
5
5
|
import { TokenTracker } from "./tracker.js";
|
|
6
6
|
import { renderToolStart, renderToolSuccess, renderToolError, renderDiff, renderFooter, renderError, } from "../ui/renderer.js";
|
|
7
7
|
import { CONTEXT_WINDOW, MAX_OUTPUT_TOKENS, COMPACT_THRESHOLD_TOKENS, } from "../config/constants.js";
|
|
8
|
-
import { withRetry } from "./retry.js";
|
|
8
|
+
import { withRetry, isContextOverflow } from "./retry.js";
|
|
9
9
|
import { canRunInParallel } from "../tools/safety.js";
|
|
10
|
-
import { createEngineClient, explicarErrorDelMotor } from "../config/engine.js";
|
|
10
|
+
import { createEngineClient, explicarErrorDelMotor, motivoDeParadaDelError, } from "../config/engine.js";
|
|
11
11
|
import { MarkdownStream } from "../ui/markdownStream.js";
|
|
12
12
|
import { checkQuota, recordUsage, mensajeDeCorte, quotaBadge } from "../config/quota.js";
|
|
13
13
|
import { ReasoningStream } from "../ui/reasoningStream.js";
|
|
14
|
-
import { PermissionManager } from "../config/permissions.js";
|
|
14
|
+
import { PermissionManager, commandSignature, isDangerousCommand } from "../config/permissions.js";
|
|
15
15
|
import { askToolPermission } from "../ui/permissionPrompt.js";
|
|
16
16
|
import { DynamicSpinner } from "../ui/spinner.js";
|
|
17
17
|
import { globalUndoManager } from "./undoManager.js";
|
|
18
18
|
import { InterruptWatcher, isAbortError } from "../ui/interrupt.js";
|
|
19
19
|
import { HookManager } from "../hooks/manager.js";
|
|
20
|
+
import { registerToolGate, autorizarHerramienta, notificarPostHerramienta } from "./toolGate.js";
|
|
21
|
+
import { DetectorDeBucle, mensajeDeBucle } from "./loopDetector.js";
|
|
22
|
+
import { preguntarPorBucle } from "../ui/loopPrompt.js";
|
|
23
|
+
import { MAX_REFLEXIONES, detectarComando, mensajeDeRendicion, mensajeDeVerificacion, verificacionAutomaticaActiva, verificar, } from "./verifier.js";
|
|
24
|
+
import { apuntarUso, tomarGastoAuxiliar } from "./usageMeter.js";
|
|
25
|
+
import { leerLimites } from "../config/limits.js";
|
|
26
|
+
import { corteSeguro, estimarTokens, limpiarHuerfanos, vaciarResultadosAntiguos, } from "./compaction.js";
|
|
27
|
+
/** Herramientas cuyo exito deja el proyecto en un estado que vale la pena verificar. */
|
|
28
|
+
const HERRAMIENTAS_QUE_ESCRIBEN = new Set([
|
|
29
|
+
"edit_file",
|
|
30
|
+
"write_file",
|
|
31
|
+
"move_copy_file",
|
|
32
|
+
"delete_file",
|
|
33
|
+
]);
|
|
34
|
+
/** Cortes por longitud seguidos que se toleran antes de rendirse. */
|
|
35
|
+
const MAX_CORTES_POR_LONGITUD = 2;
|
|
36
|
+
/** Mensajes recientes que la compactacion no resume nunca. */
|
|
37
|
+
const COLA_INTACTA = 4;
|
|
38
|
+
/**
|
|
39
|
+
* Mensajes recientes cuyas salidas de herramienta se conservan enteras. Las
|
|
40
|
+
* anteriores se vacian antes de plantearse siquiera resumir.
|
|
41
|
+
*/
|
|
42
|
+
const RESULTADOS_INTACTOS = 12;
|
|
43
|
+
/**
|
|
44
|
+
* Argumentos de una llamada a herramienta, distinguiendo el caso que importa.
|
|
45
|
+
*
|
|
46
|
+
* Un `JSON.parse` que falla no significa "esta herramienta no lleva
|
|
47
|
+
* argumentos": significa que el modelo se quedo sin presupuesto de salida a
|
|
48
|
+
* mitad de escribirlos. Tratar las dos cosas igual —que es lo que se hacia,
|
|
49
|
+
* con un `catch { fnArgs = {} }`— ejecuta la herramienta con las manos vacias:
|
|
50
|
+
* un write_file sin ruta ni contenido, un run_command sin comando. Y responde
|
|
51
|
+
* como si nada.
|
|
52
|
+
*/
|
|
53
|
+
function parsearArgumentos(bruto) {
|
|
54
|
+
const texto = (bruto ?? "").trim();
|
|
55
|
+
// Sin argumentos de verdad: hay herramientas que no llevan ninguno.
|
|
56
|
+
if (!texto || texto === "{}")
|
|
57
|
+
return { ok: true, args: {} };
|
|
58
|
+
try {
|
|
59
|
+
const args = JSON.parse(texto);
|
|
60
|
+
if (args && typeof args === "object" && !Array.isArray(args))
|
|
61
|
+
return { ok: true, args };
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
return { ok: false, args: {} };
|
|
65
|
+
}
|
|
66
|
+
/** Lo que se le dice al modelo cuando sus argumentos llegaron a medias. */
|
|
67
|
+
function mensajeDeArgumentosRotos(herramienta) {
|
|
68
|
+
return (`No se ejecuto "${herramienta}": los argumentos llegaron incompletos, seguramente porque la ` +
|
|
69
|
+
`respuesta se corto por longitud. No se ha hecho nada. Vuelve a pedirlo, y si el contenido es ` +
|
|
70
|
+
`largo, escribelo en varias llamadas mas pequenas en vez de una sola.`);
|
|
71
|
+
}
|
|
20
72
|
export class AgentLoop {
|
|
21
73
|
client;
|
|
74
|
+
/**
|
|
75
|
+
* Cliente aparte para resumir el historial. No es cosmetico: la cabecera
|
|
76
|
+
* X-Chocolatito-Route es lo que le permite al proxy saber en que se le va la
|
|
77
|
+
* cuota al usuario, y con un unico cliente las compactaciones —que son las
|
|
78
|
+
* llamadas mas caras que hace el agente— llegaban etiquetadas como "loop".
|
|
79
|
+
*/
|
|
80
|
+
compactClient;
|
|
22
81
|
currentEspectro;
|
|
23
82
|
messages = [];
|
|
24
83
|
tracker;
|
|
@@ -27,17 +86,36 @@ export class AgentLoop {
|
|
|
27
86
|
hooks;
|
|
28
87
|
apiKey;
|
|
29
88
|
baseURL;
|
|
30
|
-
|
|
89
|
+
// El presupuesto de vueltas ya no esta escrito en el codigo: se lee de la
|
|
90
|
+
// configuracion del usuario, que es quien conoce su tarea y su cuota.
|
|
91
|
+
maxIterations = leerLimites().iteracionesPorTurno;
|
|
92
|
+
/**
|
|
93
|
+
* Vigila que el agente no se quede dando vueltas. Vive en la instancia y no
|
|
94
|
+
* en runTask porque el estado tiene que sobrevivir a las iteraciones del
|
|
95
|
+
* while; se reinicia en cada turno nuevo.
|
|
96
|
+
*/
|
|
97
|
+
detectorBucle = new DetectorDeBucle();
|
|
98
|
+
/**
|
|
99
|
+
* Archivos escritos en el turno. Solo sirve para saber si hay algo que
|
|
100
|
+
* verificar al cerrarlo: si el turno no toco el disco, no hay nada que
|
|
101
|
+
* comprobar y correr el compilador seria tiempo del usuario tirado.
|
|
102
|
+
*/
|
|
103
|
+
archivosTocados = new Set();
|
|
31
104
|
constructor(apiKey, baseURL, initialModel, systemPrompt) {
|
|
32
105
|
this.apiKey = apiKey;
|
|
33
106
|
this.baseURL = baseURL;
|
|
34
107
|
// Un unico sitio construye el cliente: asi la credencial de suscripcion y
|
|
35
108
|
// las cabeceras propias llegan igual desde las cuatro rutas.
|
|
36
109
|
this.client = createEngineClient(apiKey, "loop");
|
|
110
|
+
this.compactClient = createEngineClient(apiKey, "compact");
|
|
37
111
|
this.currentEspectro = initialModel;
|
|
38
112
|
this.tracker = new TokenTracker();
|
|
39
113
|
this.permissions = new PermissionManager("default", this.currentCwd);
|
|
40
114
|
this.hooks = new HookManager(this.currentCwd);
|
|
115
|
+
// Los subagentes ejecutan herramientas fuera de este bucle, y estos dos
|
|
116
|
+
// gestores no les llegan por ninguna firma. Sin registrarlos aqui, corren sin
|
|
117
|
+
// permisos, sin hooks y sin el bloqueo del modo plan.
|
|
118
|
+
registerToolGate(this.permissions, this.hooks);
|
|
41
119
|
this.messages.push({
|
|
42
120
|
role: "system",
|
|
43
121
|
content: systemPrompt,
|
|
@@ -73,11 +151,14 @@ export class AgentLoop {
|
|
|
73
151
|
content: "Genera el resumen ejecutivo del contexto de trabajo hasta el momento.",
|
|
74
152
|
},
|
|
75
153
|
];
|
|
76
|
-
const res = await this.
|
|
154
|
+
const res = await this.compactClient.chat.completions.create({
|
|
77
155
|
model: this.currentEspectro.underlyingModel,
|
|
78
156
|
messages: summaryPrompt,
|
|
79
157
|
temperature: 0.1,
|
|
80
158
|
});
|
|
159
|
+
// Resumir manda el historial ENTERO como prompt. Es dinero real y hasta
|
|
160
|
+
// ahora no lo apuntaba nadie: el pie seguia marcando el mismo porcentaje.
|
|
161
|
+
apuntarUso(res.usage, this.currentEspectro.underlyingModel, "compact");
|
|
81
162
|
const summary = res.choices[0]?.message?.content || "Contexto previo compactado.";
|
|
82
163
|
spinner.stop();
|
|
83
164
|
this.messages = [
|
|
@@ -98,41 +179,90 @@ export class AgentLoop {
|
|
|
98
179
|
return `Fallo al compactar historial: ${err?.message || String(err)}`;
|
|
99
180
|
}
|
|
100
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Tokens que OCUPA el historial ahora mismo, no los que se han gastado en el
|
|
184
|
+
* turno. Son dos numeros distintos y confundirlos era el fallo de abajo.
|
|
185
|
+
*/
|
|
101
186
|
lastTurnTokens = 0;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Compactacion automatica del contexto.
|
|
189
|
+
*
|
|
190
|
+
* Habia tres cosas mal, y cada una se comia el contexto por su lado:
|
|
191
|
+
*
|
|
192
|
+
* 1. El corte era por posicion fija: slice(1, -4) para lo que se resumia y
|
|
193
|
+
* slice(-4) para lo que se conservaba. Un turno que acaba en un bloque de
|
|
194
|
+
* cuatro herramientas deja la cola empezando por un mensaje "tool" cuya
|
|
195
|
+
* llamada se ha quedado en la mitad resumida. La API contesta 400 y el
|
|
196
|
+
* catch mudo lo tapaba: la sesion se rompia sin motivo visible, y siempre
|
|
197
|
+
* en las sesiones largas, que son las que llevan mas trabajo encima. El
|
|
198
|
+
* corte lo decide ahora la forma del historial, nunca una posicion.
|
|
199
|
+
*
|
|
200
|
+
* 2. El umbral solo se miraba al empezar el turno y sobre el dato del turno
|
|
201
|
+
* ANTERIOR. Un turno de veinte herramientas puede doblar el contexto sin
|
|
202
|
+
* que nadie lo compruebe, y el limite se choca a mitad de camino.
|
|
203
|
+
*
|
|
204
|
+
* 3. Resumir cuesta una llamada al modelo y pierde detalle. Vaciar las
|
|
205
|
+
* salidas de herramienta viejas no cuesta nada y casi siempre basta, asi
|
|
206
|
+
* que va primero.
|
|
207
|
+
*/
|
|
208
|
+
async autoCompactIfNeeded(tokensActuales = this.lastTurnTokens) {
|
|
209
|
+
// La ventana real es de 1.000.000 de tokens. Comprimir antes de acercarse
|
|
210
|
+
// al limite es tirar contexto bueno.
|
|
211
|
+
if (tokensActuales < COMPACT_THRESHOLD_TOKENS)
|
|
108
212
|
return false;
|
|
213
|
+
// Paso 1, gratis: vaciar las salidas de herramienta mas viejas. Un view_file
|
|
214
|
+
// de hace veinte pasos ocupa miles de tokens y ya no dice nada; las
|
|
215
|
+
// decisiones tomadas, que son lo que hay que conservar, estan en el texto.
|
|
216
|
+
const antes = estimarTokens(this.messages);
|
|
217
|
+
const podado = vaciarResultadosAntiguos(this.messages, RESULTADOS_INTACTOS);
|
|
218
|
+
if (podado.liberados > 0) {
|
|
219
|
+
this.messages = podado.mensajes;
|
|
220
|
+
const despues = estimarTokens(this.messages);
|
|
221
|
+
// La cifra absoluta buena la da la API en prompt_tokens; de la estimacion
|
|
222
|
+
// solo se usa la proporcion de lo que se acaba de quitar.
|
|
223
|
+
const proyectado = antes > 0 ? Math.round(tokensActuales * (despues / antes)) : tokensActuales;
|
|
224
|
+
this.lastTurnTokens = proyectado;
|
|
225
|
+
if (proyectado < COMPACT_THRESHOLD_TOKENS * 0.8) {
|
|
226
|
+
console.log(chalk.hex("#D97757")("\n⚡ [Auto-Compact]: ") +
|
|
227
|
+
chalk.gray(`Liberados ~${(antes - despues).toLocaleString()} tokens de salidas de herramientas antiguas. ` +
|
|
228
|
+
"La conversacion se conserva entera.\n"));
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
109
231
|
}
|
|
232
|
+
const spinner = new DynamicSpinner();
|
|
110
233
|
try {
|
|
111
234
|
const activeSystemPrompt = this.messages[0]?.role === "system" ? this.messages[0].content : "";
|
|
112
|
-
|
|
113
|
-
|
|
235
|
+
// El corte respeta siempre los pares llamada-respuesta; si el punto que
|
|
236
|
+
// tocaba parte un bloque, se conserva de mas.
|
|
237
|
+
const corte = corteSeguro(this.messages, COLA_INTACTA);
|
|
238
|
+
const cabeza = limpiarHuerfanos(this.messages.slice(1, corte));
|
|
239
|
+
if (cabeza.length < 4)
|
|
114
240
|
return false;
|
|
115
|
-
const
|
|
241
|
+
const cola = this.messages.slice(corte);
|
|
116
242
|
spinner.start("Auto-optimizando memoria del contexto");
|
|
117
243
|
const summaryPrompt = [
|
|
118
244
|
{
|
|
119
245
|
role: "system",
|
|
120
246
|
content: "Resume de forma ultra concisa los archivos modificados, decisiones tomadas, arquitectura y estado actual del proyecto para continuar trabajando sin perder el hilo.",
|
|
121
247
|
},
|
|
122
|
-
...
|
|
248
|
+
...cabeza,
|
|
123
249
|
{
|
|
124
250
|
role: "user",
|
|
125
251
|
content: "Genera el resumen ejecutivo del contexto de trabajo previo.",
|
|
126
252
|
},
|
|
127
253
|
];
|
|
128
|
-
const res = await this.
|
|
254
|
+
const res = await this.compactClient.chat.completions.create({
|
|
129
255
|
model: this.currentEspectro.underlyingModel,
|
|
130
256
|
messages: summaryPrompt,
|
|
131
257
|
temperature: 0.1,
|
|
132
258
|
});
|
|
259
|
+
// La llamada mas cara del agente: entra aqui con el contexto rozando el
|
|
260
|
+
// umbral (750.000 tokens) y lo manda entero. En Espectro 2.0 son ~0,99 $,
|
|
261
|
+
// el 85% del presupuesto semanal por defecto, que antes valian 0,00 $ en
|
|
262
|
+
// el contador local. De ahi el 402 por sorpresa.
|
|
263
|
+
apuntarUso(res.usage, this.currentEspectro.underlyingModel, "compact");
|
|
133
264
|
const summary = res.choices[0]?.message?.content || "Contexto previo optimizado.";
|
|
134
265
|
spinner.stop();
|
|
135
|
-
const recentMessages = this.messages.slice(-4);
|
|
136
266
|
this.messages = [
|
|
137
267
|
{ role: "system", content: activeSystemPrompt },
|
|
138
268
|
{
|
|
@@ -143,38 +273,184 @@ export class AgentLoop {
|
|
|
143
273
|
role: "assistant",
|
|
144
274
|
content: "Entendido. He retenido el contexto clave del proyecto y liberado memoria.",
|
|
145
275
|
},
|
|
146
|
-
...
|
|
276
|
+
...cola,
|
|
147
277
|
];
|
|
278
|
+
// Queda una estimacion hasta la siguiente respuesta de la API: mejor eso
|
|
279
|
+
// que arrastrar la cifra vieja, que dispararia otra compactacion seguida.
|
|
280
|
+
this.lastTurnTokens = estimarTokens(this.messages);
|
|
148
281
|
console.log(chalk.hex("#D97757")("\n⚡ [Auto-Compact]: ") +
|
|
149
282
|
chalk.gray(`Contexto optimizado automáticamente (${this.messages.length} mensajes limpios) para mantener alta velocidad y mínimo consumo de API.\n`));
|
|
150
283
|
return true;
|
|
151
284
|
}
|
|
152
|
-
catch {
|
|
285
|
+
catch (err) {
|
|
286
|
+
spinner.stop();
|
|
287
|
+
// El catch mudo era medio fallo: si el resumen no sale, el turno sigue con
|
|
288
|
+
// el contexto entero y el error de despues llega sin explicacion.
|
|
289
|
+
console.log(chalk.gray(` (no se pudo compactar el contexto: ${err?.message || String(err)})`));
|
|
153
290
|
return false;
|
|
154
291
|
}
|
|
155
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Verificacion del proyecto al cerrar un turno que escribio en disco.
|
|
295
|
+
*
|
|
296
|
+
* Devuelve el mensaje que hay que reinyectarle al modelo, o null si no hay
|
|
297
|
+
* nada que corregir (o si no toca verificar). El tope de vueltas evita el
|
|
298
|
+
* otro extremo: un agente peleandose solo con el compilador durante veinte
|
|
299
|
+
* iteraciones mientras el usuario mira.
|
|
300
|
+
*/
|
|
301
|
+
async verificarTrasEditar(reflexiones) {
|
|
302
|
+
if (this.archivosTocados.size === 0)
|
|
303
|
+
return null;
|
|
304
|
+
if (!verificacionAutomaticaActiva(this.currentCwd))
|
|
305
|
+
return null;
|
|
306
|
+
const cmd = detectarComando(this.currentCwd);
|
|
307
|
+
if (!cmd)
|
|
308
|
+
return null;
|
|
309
|
+
if (reflexiones >= MAX_REFLEXIONES) {
|
|
310
|
+
console.log(chalk.yellow(`\n⚠ ${mensajeDeRendicion(cmd.comando)}\n`));
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
const spinner = new DynamicSpinner();
|
|
314
|
+
spinner.start(`Verificando (${cmd.comando})`);
|
|
315
|
+
let resultado;
|
|
316
|
+
try {
|
|
317
|
+
resultado = await verificar(cmd.comando, this.currentCwd);
|
|
318
|
+
}
|
|
319
|
+
finally {
|
|
320
|
+
spinner.stop();
|
|
321
|
+
}
|
|
322
|
+
if (resultado.ok) {
|
|
323
|
+
console.log(chalk.gray(` ✔ Verificado con ${cmd.comando}\n`));
|
|
324
|
+
// Lo verificado deja de estar pendiente: si el modelo vuelve a editar,
|
|
325
|
+
// se vuelve a comprobar; si no, no se comprueba dos veces lo mismo.
|
|
326
|
+
this.archivosTocados.clear();
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
console.log(chalk.yellow(`\n⚠ La verificacion fallo (${cmd.comando}). Se lo devuelvo al agente.\n`));
|
|
330
|
+
return mensajeDeVerificacion(cmd.comando, resultado.salida, reflexiones + 1);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Comprueba si esta llamada cierra un bucle y, si lo cierra, decide con el
|
|
334
|
+
* usuario que hacer. Devuelve el texto que se le pasa al modelo: el resultado
|
|
335
|
+
* tal cual, o la explicacion del atasco.
|
|
336
|
+
*
|
|
337
|
+
* Se llama DESPUES de ejecutar, no antes, porque la firma incluye el
|
|
338
|
+
* resultado: sin el, un sondeo legitimo (releer un log que cambia) parece un
|
|
339
|
+
* bucle. La tercera llamada repetida ya se ha pagado; lo que se evita son la
|
|
340
|
+
* cuarta y las veintiseis siguientes.
|
|
341
|
+
*/
|
|
342
|
+
/** Apunta que este turno ha escrito en disco, para verificarlo al cerrarlo. */
|
|
343
|
+
apuntarEscritura(herramienta, args, resultado) {
|
|
344
|
+
if (!HERRAMIENTAS_QUE_ESCRIBEN.has(herramienta))
|
|
345
|
+
return;
|
|
346
|
+
// Un edit_file que fallo no ha cambiado nada: verificar por eso seria
|
|
347
|
+
// hacerle perder tiempo al usuario por un error que ya se conto.
|
|
348
|
+
if (resultado.startsWith("Error") || resultado.includes("bloqueada:"))
|
|
349
|
+
return;
|
|
350
|
+
const ruta = args?.path || args?.destinationPath || args?.sourcePath;
|
|
351
|
+
if (typeof ruta === "string")
|
|
352
|
+
this.archivosTocados.add(ruta);
|
|
353
|
+
}
|
|
354
|
+
async vigilarBucle(herramienta, argumentos, resultado, antesDePreguntar) {
|
|
355
|
+
const aviso = this.detectorBucle.registrar(herramienta, argumentos, resultado);
|
|
356
|
+
if (!aviso)
|
|
357
|
+
return resultado;
|
|
358
|
+
antesDePreguntar?.();
|
|
359
|
+
const respuesta = await preguntarPorBucle(aviso);
|
|
360
|
+
if (respuesta.accion === "seguir") {
|
|
361
|
+
this.detectorBucle.perdonar(herramienta);
|
|
362
|
+
return resultado;
|
|
363
|
+
}
|
|
364
|
+
this.detectorBucle.olvidar();
|
|
365
|
+
return mensajeDeBucle(aviso, respuesta.indicacion);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Ejecuta un turno completo y CUENTA como acabo.
|
|
369
|
+
*
|
|
370
|
+
* Antes devolvia void: el motivo del corte se imprimia por pantalla y ahi se
|
|
371
|
+
* moria. Quien llamaba no podia distinguir "terminado" de "cuota agotada al
|
|
372
|
+
* primer intento", que es justo lo que necesita el modo no interactivo para
|
|
373
|
+
* salir con un codigo honesto.
|
|
374
|
+
*/
|
|
156
375
|
async runTask(userPrompt) {
|
|
376
|
+
let iterations = 0;
|
|
377
|
+
// COSTE: se paga cada llamada, asi que los prompts se suman.
|
|
378
|
+
let turnPromptTokens = 0;
|
|
379
|
+
// CONTEXTO: lo que ocupa el historial es el prompt de la ULTIMA llamada,
|
|
380
|
+
// no la suma de todas. Con la suma, un turno de diez llamadas parecia diez
|
|
381
|
+
// veces mas lleno de lo que estaba.
|
|
382
|
+
let lastCallPromptTokens = 0;
|
|
383
|
+
let turnCachedTokens = 0;
|
|
384
|
+
let turnCompletionTokens = 0;
|
|
385
|
+
/**
|
|
386
|
+
* Unica salida de runTask, y por eso el sitio donde se cierra el checkpoint
|
|
387
|
+
* de /undo.
|
|
388
|
+
*
|
|
389
|
+
* commitTurn vivia despues del while, asi que ninguna de las salidas
|
|
390
|
+
* anticipadas —Esc a mitad de turno, error del motor, cuota agotada— pasaba
|
|
391
|
+
* por el. Consecuencia doble y las dos malas: el usuario que interrumpia un
|
|
392
|
+
* turno que ya habia reescrito tres archivos leia "no hay acciones previas
|
|
393
|
+
* para deshacer" con el disco a medias; y esas instantaneas huerfanas se
|
|
394
|
+
* quedaban en currentTurnSnapshots (snapshotFile hace early-return si la
|
|
395
|
+
* clave ya existe, asi que ni se refrescaban) hasta colgarse del primer
|
|
396
|
+
* turno que terminara limpio, que al deshacerse revertia tambien los
|
|
397
|
+
* archivos del turno interrumpido anunciando otra descripcion.
|
|
398
|
+
*
|
|
399
|
+
* El motivo va en la descripcion: en el historial de /undo hay que poder
|
|
400
|
+
* distinguir un turno terminado de uno cortado.
|
|
401
|
+
*/
|
|
402
|
+
const parada = (motivo, detalle) => {
|
|
403
|
+
globalUndoManager.commitTurn(motivo === "completado" ? userPrompt : `${userPrompt} [${motivo}]`);
|
|
404
|
+
return {
|
|
405
|
+
exito: motivo === "completado",
|
|
406
|
+
motivo,
|
|
407
|
+
detalle,
|
|
408
|
+
turnos: iterations,
|
|
409
|
+
tokensEntrada: turnPromptTokens,
|
|
410
|
+
tokensSalida: turnCompletionTokens,
|
|
411
|
+
};
|
|
412
|
+
};
|
|
157
413
|
// Cuota ANTES de gastar. Si no queda margen, se dice y no se llama al motor:
|
|
158
414
|
// avisar despues de haber gastado no sirve de nada.
|
|
159
415
|
const cuota = checkQuota();
|
|
160
416
|
if (!cuota.allowed) {
|
|
161
|
-
|
|
417
|
+
const corte = mensajeDeCorte(cuota);
|
|
418
|
+
console.log(chalk.yellow(`\n⏸ ${corte}`) +
|
|
162
419
|
chalk.gray("\n Tu trabajo y tu contexto siguen aqui: al liberarse, continuas donde lo dejaste.\n"));
|
|
163
|
-
return;
|
|
420
|
+
return parada("cuota", corte);
|
|
164
421
|
}
|
|
165
422
|
// Check and execute auto-compaction if context threshold is exceeded
|
|
166
423
|
await this.autoCompactIfNeeded();
|
|
424
|
+
// El usuario ha dicho algo nuevo: lo que se repitiera en el turno anterior
|
|
425
|
+
// ya no cuenta, y lo que perdono tampoco tiene por que valer aqui.
|
|
426
|
+
this.detectorBucle.reiniciar();
|
|
167
427
|
const startTime = Date.now();
|
|
168
428
|
this.messages.push({
|
|
169
429
|
role: "user",
|
|
170
430
|
content: userPrompt,
|
|
171
431
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
let
|
|
175
|
-
|
|
432
|
+
// Distingue "el modelo cerro su turno" de "se acabaron las iteraciones",
|
|
433
|
+
// que por fuera se parecen y no valen lo mismo.
|
|
434
|
+
let completado = false;
|
|
435
|
+
/** Un desbordamiento de contexto se rescata compactando, pero una sola vez. */
|
|
436
|
+
let yaCompactadoPorDesbordamiento = false;
|
|
437
|
+
/** Vueltas gastadas en corregir lo que dijo el verificador del proyecto. */
|
|
438
|
+
let reflexiones = 0;
|
|
439
|
+
/** Veces seguidas que la respuesta se corto por longitud sin hacer nada. */
|
|
440
|
+
let cortesPorLongitud = 0;
|
|
441
|
+
// Se limpia aqui: lo que se edito en el turno anterior ya se verifico (o no
|
|
442
|
+
// se iba a verificar nunca).
|
|
443
|
+
this.archivosTocados.clear();
|
|
176
444
|
while (iterations < this.maxIterations) {
|
|
177
445
|
iterations++;
|
|
446
|
+
// El contexto tambien crece dentro del turno: veinte herramientas con
|
|
447
|
+
// sus salidas pueden pasar del umbral sin que termine el turno. Mirarlo
|
|
448
|
+
// solo al principio era llegar tarde.
|
|
449
|
+
if (lastCallPromptTokens > 0) {
|
|
450
|
+
const compactado = await this.autoCompactIfNeeded(lastCallPromptTokens);
|
|
451
|
+
if (compactado)
|
|
452
|
+
lastCallPromptTokens = this.lastTurnTokens;
|
|
453
|
+
}
|
|
178
454
|
// Near iteration limit notice
|
|
179
455
|
if (iterations === this.maxIterations - 2) {
|
|
180
456
|
this.messages.push({
|
|
@@ -213,23 +489,37 @@ export class AgentLoop {
|
|
|
213
489
|
watcher.stop();
|
|
214
490
|
if (isAbortError(err) || watcher.interrupted) {
|
|
215
491
|
console.log(chalk.yellow("\n⏸ Interrumpido. Dime como seguir."));
|
|
216
|
-
return;
|
|
492
|
+
return parada("interrumpido");
|
|
493
|
+
}
|
|
494
|
+
// El contexto no cabe. Reintentar la misma peticion es imposible por
|
|
495
|
+
// definicion, pero compactar y volver a intentarlo si tiene arreglo, y
|
|
496
|
+
// es justo lo que el usuario esperaria en vez de perder el turno. Una
|
|
497
|
+
// sola vez: si tras compactar sigue sin caber, el problema es otro.
|
|
498
|
+
if (isContextOverflow(err) && !yaCompactadoPorDesbordamiento) {
|
|
499
|
+
yaCompactadoPorDesbordamiento = true;
|
|
500
|
+
console.log(chalk.yellow("\n⚠ La peticion no cabia en el contexto. Compactando y reintentando..."));
|
|
501
|
+
const activo = this.messages[0]?.role === "system" ? this.messages[0].content : "";
|
|
502
|
+
await this.compactHistory(activo);
|
|
503
|
+
iterations--; // este intento no llego al modelo: no cuenta como vuelta
|
|
504
|
+
continue;
|
|
217
505
|
}
|
|
218
506
|
// Un 402 (cuota agotada) o un 401 (licencia caida) no son "fallos del
|
|
219
507
|
// motor" y ademas no se reintentan nunca: decirlo como tal confunde y
|
|
220
508
|
// hace que el usuario piense que el producto esta roto.
|
|
221
509
|
const claro = explicarErrorDelMotor(err);
|
|
222
510
|
if (claro) {
|
|
223
|
-
console.log(chalk.yellow(`
|
|
224
|
-
⏸ ${claro}
|
|
511
|
+
console.log(chalk.yellow(`
|
|
512
|
+
⏸ ${claro}
|
|
225
513
|
`));
|
|
226
|
-
return;
|
|
514
|
+
return parada(motivoDeParadaDelError(err), claro);
|
|
227
515
|
}
|
|
228
|
-
|
|
229
|
-
|
|
516
|
+
const fallo = `Error en el motor tras varios reintentos: ${err?.message || String(err)}`;
|
|
517
|
+
renderError(fallo);
|
|
518
|
+
return parada("error-motor", fallo);
|
|
230
519
|
}
|
|
231
520
|
let content = "";
|
|
232
521
|
let reasoning = "";
|
|
522
|
+
let motivoDeCorte = null;
|
|
233
523
|
const rawToolCalls = [];
|
|
234
524
|
let isFirstChunk = true;
|
|
235
525
|
const reasoningStream = new ReasoningStream();
|
|
@@ -272,10 +562,17 @@ export class AgentLoop {
|
|
|
272
562
|
rawToolCalls[idx].function.arguments += tc.function.arguments;
|
|
273
563
|
}
|
|
274
564
|
}
|
|
565
|
+
// El motivo por el que el modelo dejo de hablar. Sin esto, quedarse
|
|
566
|
+
// sin presupuesto de salida es indistinguible de terminar el turno.
|
|
567
|
+
const corte = chunk.choices?.[0]?.finish_reason;
|
|
568
|
+
if (corte)
|
|
569
|
+
motivoDeCorte = corte;
|
|
275
570
|
// Track streaming usage metrics
|
|
276
571
|
if (chunk.usage) {
|
|
277
572
|
this.tracker.addUsage(chunk.usage);
|
|
278
573
|
turnPromptTokens += chunk.usage.prompt_tokens || 0;
|
|
574
|
+
if (chunk.usage.prompt_tokens)
|
|
575
|
+
lastCallPromptTokens = chunk.usage.prompt_tokens;
|
|
279
576
|
turnCompletionTokens += chunk.usage.completion_tokens || 0;
|
|
280
577
|
turnCachedTokens += chunk.usage.prompt_tokens_details?.cached_tokens || 0;
|
|
281
578
|
}
|
|
@@ -293,17 +590,18 @@ export class AgentLoop {
|
|
|
293
590
|
this.messages.push({ role: "assistant", content });
|
|
294
591
|
}
|
|
295
592
|
console.log(chalk.yellow("\n⏸ Interrumpido. Dime como seguir."));
|
|
296
|
-
return;
|
|
593
|
+
return parada("interrumpido");
|
|
297
594
|
}
|
|
298
595
|
const claroStream = explicarErrorDelMotor(streamErr);
|
|
299
596
|
if (claroStream) {
|
|
300
|
-
console.log(chalk.yellow(`
|
|
301
|
-
⏸ ${claroStream}
|
|
597
|
+
console.log(chalk.yellow(`
|
|
598
|
+
⏸ ${claroStream}
|
|
302
599
|
`));
|
|
303
|
-
return;
|
|
600
|
+
return parada(motivoDeParadaDelError(streamErr), claroStream);
|
|
304
601
|
}
|
|
305
|
-
|
|
306
|
-
|
|
602
|
+
const falloStream = `Error en streaming: ${streamErr?.message || String(streamErr)}`;
|
|
603
|
+
renderError(falloStream);
|
|
604
|
+
return parada("error-motor", falloStream);
|
|
307
605
|
}
|
|
308
606
|
spinner.stop();
|
|
309
607
|
reasoningStream.flush();
|
|
@@ -313,10 +611,46 @@ export class AgentLoop {
|
|
|
313
611
|
if (content.trim())
|
|
314
612
|
this.messages.push({ role: "assistant", content });
|
|
315
613
|
console.log(chalk.yellow("\n⏸ Interrumpido antes de ejecutar las herramientas. Dime como seguir."));
|
|
316
|
-
return;
|
|
614
|
+
return parada("interrumpido");
|
|
317
615
|
}
|
|
318
616
|
// Filter valid accumulated tool calls
|
|
319
617
|
const toolCalls = rawToolCalls.filter((tc) => tc && tc.function && tc.function.name);
|
|
618
|
+
// El modelo se quedo sin presupuesto de salida y no llego a pedir nada.
|
|
619
|
+
//
|
|
620
|
+
// Esto se veia EXACTAMENTE igual que un turno terminado: sin llamadas a
|
|
621
|
+
// herramienta, el bucle daba el turno por cerrado, imprimia el pie y
|
|
622
|
+
// volvia al prompt. El usuario pedia "creame un juego en un archivo html",
|
|
623
|
+
// el modelo gastaba los 16.384 tokens razonando el diseno, se cortaba a
|
|
624
|
+
// mitad de frase, y no se creaba ningun archivo ni se decia por que.
|
|
625
|
+
//
|
|
626
|
+
// Se le devuelve el aviso y se le pide que actue, con tope: si a la
|
|
627
|
+
// segunda sigue sin hacer nada, el problema no es el presupuesto.
|
|
628
|
+
if (motivoDeCorte === "length" && toolCalls.length === 0) {
|
|
629
|
+
cortesPorLongitud++;
|
|
630
|
+
if (cortesPorLongitud > MAX_CORTES_POR_LONGITUD) {
|
|
631
|
+
const aviso = `La respuesta se corto por longitud ${cortesPorLongitud} veces seguidas sin llegar a ` +
|
|
632
|
+
`hacer nada. Prueba a pedirlo por partes.`;
|
|
633
|
+
console.log(chalk.yellow(`\n ⚠ ${aviso}\n`));
|
|
634
|
+
if (content.trim())
|
|
635
|
+
this.messages.push({ role: "assistant", content });
|
|
636
|
+
return parada("limite-iteraciones", aviso);
|
|
637
|
+
}
|
|
638
|
+
console.log(chalk.yellow("\n ⚠ La respuesta se corto por longitud antes de hacer nada. Pidiendole que vaya al grano...\n"));
|
|
639
|
+
if (content.trim())
|
|
640
|
+
this.messages.push({ role: "assistant", content });
|
|
641
|
+
this.messages.push({
|
|
642
|
+
role: "user",
|
|
643
|
+
content: "Tu respuesta anterior se corto porque agoto el presupuesto de salida mientras razonabas, " +
|
|
644
|
+
"y no llegaste a usar ninguna herramienta: no se ha hecho nada todavia. Deja de planificar " +
|
|
645
|
+
"y actua ya. Si lo que hay que escribir es largo, hazlo en varias llamadas mas pequenas.",
|
|
646
|
+
});
|
|
647
|
+
// Sin esto se queda un vigilante de teclado vivo por cada corte, y cada
|
|
648
|
+
// uno vuelve a apuntarse como oyente: la misma tecla se procesa tantas
|
|
649
|
+
// veces como vigilantes haya, y el teclado deja de responder como
|
|
650
|
+
// deberia. La regla es: de este bucle no se sale sin soltar el teclado.
|
|
651
|
+
watcher.stop();
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
320
654
|
if (toolCalls.length > 0) {
|
|
321
655
|
this.messages.push({
|
|
322
656
|
role: "assistant",
|
|
@@ -330,40 +664,77 @@ export class AgentLoop {
|
|
|
330
664
|
const names = toolCalls.map((tc) => tc.function.name);
|
|
331
665
|
const parallel = canRunInParallel(names) &&
|
|
332
666
|
!toolCalls.some((tc) => {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return this.permissions.requiresConfirmation(tc.function.name,
|
|
667
|
+
const parsed = parsearArgumentos(tc.function.arguments);
|
|
668
|
+
// Unos argumentos rotos se resuelven en serie, donde hay sitio para
|
|
669
|
+
// explicarselo al modelo.
|
|
670
|
+
if (!parsed.ok)
|
|
671
|
+
return true;
|
|
672
|
+
return this.permissions.requiresConfirmation(tc.function.name, parsed.args, this.currentCwd);
|
|
339
673
|
});
|
|
340
674
|
if (parallel) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
675
|
+
// Esta rama ejecutaba directamente, sin pasar por hooks ni por deny.
|
|
676
|
+
// Un PreToolUse que prohibe leer secretos se saltaba en silencio en
|
|
677
|
+
// cuanto el modelo pedia dos lecturas en el mismo turno —cosa que el
|
|
678
|
+
// propio prompt del sistema fomenta—, y los PostToolUse (linters,
|
|
679
|
+
// politicas) tampoco corrian. Fallaba callado y hacia el lado
|
|
680
|
+
// inseguro, con lo que la politica del proyecto valia o no segun
|
|
681
|
+
// cuantas herramientas pidiera el modelo a la vez.
|
|
682
|
+
//
|
|
683
|
+
// La puerta es la misma que usan la via secuencial y los subagentes
|
|
684
|
+
// (toolGate.ts). Se autoriza en serie —los hooks son procesos, y un
|
|
685
|
+
// dialogo simultaneo por duplicado no se puede ni leer— y se
|
|
686
|
+
// paraleliza solo la ejecucion, que es lo que se queria ganar.
|
|
687
|
+
const autorizadas = [];
|
|
688
|
+
const enOrden = toolCalls.map(() => null);
|
|
689
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
690
|
+
const tc = toolCalls[i];
|
|
691
|
+
const parsed = parsearArgumentos(tc.function.arguments);
|
|
692
|
+
if (!parsed.ok) {
|
|
693
|
+
const motivo = mensajeDeArgumentosRotos(tc.function.name);
|
|
694
|
+
renderToolStart(tc.function.name, "argumentos incompletos");
|
|
695
|
+
renderToolError(tc.function.name, motivo);
|
|
696
|
+
enOrden[i] = { role: "tool", tool_call_id: tc.id, content: motivo };
|
|
697
|
+
continue;
|
|
345
698
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
699
|
+
const a = parsed.args;
|
|
700
|
+
const decision = await autorizarHerramienta(tc.function.name, a, this.currentCwd);
|
|
701
|
+
if (!decision.permitida) {
|
|
702
|
+
const motivo = decision.motivo || "Accion no autorizada.";
|
|
703
|
+
renderToolStart(tc.function.name, getToolSummary(tc.function.name, a));
|
|
704
|
+
renderToolError(tc.function.name, motivo);
|
|
705
|
+
enOrden[i] = { role: "tool", tool_call_id: tc.id, content: motivo };
|
|
706
|
+
continue;
|
|
353
707
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
708
|
+
autorizadas.push({ tc, args: decision.args, pos: i });
|
|
709
|
+
}
|
|
710
|
+
for (const { tc, args } of autorizadas) {
|
|
711
|
+
renderToolStart(tc.function.name, getToolSummary(tc.function.name, args));
|
|
712
|
+
}
|
|
713
|
+
const settled = await Promise.all(autorizadas.map(async ({ tc, args, pos }) => {
|
|
714
|
+
const r = await executeToolCall(tc.function.name, args, this.currentCwd, this.apiKey, this.currentEspectro.underlyingModel);
|
|
715
|
+
return { tc, args, pos, result: r.result };
|
|
357
716
|
}));
|
|
358
|
-
for (const { tc, result } of settled) {
|
|
717
|
+
for (const { tc, args, pos, result } of settled) {
|
|
359
718
|
renderToolSuccess(tc.function.name, result);
|
|
360
|
-
|
|
719
|
+
const aviso = await notificarPostHerramienta(tc.function.name, args, result, this.currentCwd);
|
|
720
|
+
this.apuntarEscritura(tc.function.name, args, result);
|
|
721
|
+
const texto = await this.vigilarBucle(tc.function.name, tc.function.arguments, result);
|
|
722
|
+
enOrden[pos] = {
|
|
723
|
+
role: "tool",
|
|
724
|
+
tool_call_id: tc.id,
|
|
725
|
+
content: aviso ? `${texto}\n\n[Hook PostToolUse]: ${aviso}` : texto,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
// En el orden en que las pidio el modelo, no en el que acabaron.
|
|
729
|
+
for (const r of enOrden) {
|
|
730
|
+
if (r)
|
|
731
|
+
toolResults.push(r);
|
|
361
732
|
}
|
|
362
733
|
toolResults.forEach((r) => this.messages.push(r));
|
|
363
734
|
watcher.stop();
|
|
364
735
|
if (watcher.interrupted) {
|
|
365
736
|
console.log(chalk.yellow("\n⏸ Interrumpido. Dime como seguir."));
|
|
366
|
-
return;
|
|
737
|
+
return parada("interrumpido");
|
|
367
738
|
}
|
|
368
739
|
continue;
|
|
369
740
|
}
|
|
@@ -380,12 +751,18 @@ export class AgentLoop {
|
|
|
380
751
|
}
|
|
381
752
|
const fnName = tc.function.name;
|
|
382
753
|
let fnArgs = {};
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
754
|
+
const parsed = parsearArgumentos(tc.function.arguments);
|
|
755
|
+
if (!parsed.ok) {
|
|
756
|
+
// Antes esto era `catch { fnArgs = {} }`: la herramienta se
|
|
757
|
+
// ejecutaba igual, sin argumentos. Un write_file sin ruta ni
|
|
758
|
+
// contenido, y respondiendo como si nada hubiera pasado.
|
|
759
|
+
const motivo = mensajeDeArgumentosRotos(fnName);
|
|
760
|
+
renderToolStart(fnName, "argumentos incompletos");
|
|
761
|
+
renderToolError(fnName, motivo);
|
|
762
|
+
toolResults.push({ role: "tool", tool_call_id: tc.id, content: motivo });
|
|
763
|
+
continue;
|
|
388
764
|
}
|
|
765
|
+
fnArgs = parsed.args;
|
|
389
766
|
// PreToolUse: el usuario puede vetar, autorizar o reescribir los
|
|
390
767
|
// argumentos ANTES de que se ejecute nada. Va antes del dialogo de
|
|
391
768
|
// permiso porque un veto no deberia ni llegar a preguntarse.
|
|
@@ -419,9 +796,25 @@ export class AgentLoop {
|
|
|
419
796
|
});
|
|
420
797
|
continue;
|
|
421
798
|
}
|
|
799
|
+
// Un patron de denyPatterns corta aqui, antes de todo lo demas: no se
|
|
800
|
+
// pregunta, no lo salva el --yes, no lo salva el modo bypass y no lo
|
|
801
|
+
// salva un hook que diga "allow". Hasta ahora esa lista se cargaba y se
|
|
802
|
+
// volvia a guardar sin que nadie la consultara nunca.
|
|
803
|
+
const denegado = this.permissions.deniedBy(fnName, fnArgs);
|
|
804
|
+
if (denegado) {
|
|
805
|
+
const motivo = `Bloqueado por denyPatterns ("${denegado}") en la configuracion del usuario.`;
|
|
806
|
+
renderToolStart(fnName, getToolSummary(fnName, fnArgs));
|
|
807
|
+
renderToolError(fnName, motivo);
|
|
808
|
+
toolResults.push({
|
|
809
|
+
role: "tool",
|
|
810
|
+
tool_call_id: tc.id,
|
|
811
|
+
content: `${motivo} No hay forma de aprobarlo desde aqui: el usuario lo prohibio en su settings.json. Busca otra via o preguntale.`,
|
|
812
|
+
});
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
422
815
|
// Permission gate
|
|
423
816
|
const saltaPermiso = pre.permissionDecision === "allow";
|
|
424
|
-
if (!saltaPermiso && this.permissions.requiresConfirmation(fnName, fnArgs)) {
|
|
817
|
+
if (!saltaPermiso && this.permissions.requiresConfirmation(fnName, fnArgs, this.currentCwd)) {
|
|
425
818
|
watcher.stop();
|
|
426
819
|
const permResult = await askToolPermission(fnName, fnArgs, this.currentCwd);
|
|
427
820
|
if (!permResult.approved) {
|
|
@@ -436,8 +829,7 @@ export class AgentLoop {
|
|
|
436
829
|
continue;
|
|
437
830
|
}
|
|
438
831
|
if (permResult.rememberPattern) {
|
|
439
|
-
|
|
440
|
-
this.permissions.saveAllowPattern(pat);
|
|
832
|
+
this.recordarPermiso(fnName, fnArgs);
|
|
441
833
|
}
|
|
442
834
|
}
|
|
443
835
|
const summary = getToolSummary(fnName, fnArgs);
|
|
@@ -453,11 +845,22 @@ export class AgentLoop {
|
|
|
453
845
|
// PostToolUse: sirve para validar lo que acaba de pasar (linters,
|
|
454
846
|
// tests, politicas) y devolverle al modelo un aviso accionable.
|
|
455
847
|
const post = await this.hooks.run("PostToolUse", { tool_name: fnName, tool_input: fnArgs, tool_response: result.slice(0, 4000) }, fnName);
|
|
848
|
+
// El systemMessage de PreToolUse si se imprimia y el de PostToolUse
|
|
849
|
+
// no: el mismo campo, del mismo hook, se veia o no segun el evento.
|
|
850
|
+
if (post.systemMessage) {
|
|
851
|
+
console.log(chalk.yellow(`\n⚑ ${post.systemMessage}`));
|
|
852
|
+
}
|
|
853
|
+
// El dialogo del bucle usa stdin, igual que el de permisos: hay que
|
|
854
|
+
// soltarle el terminal al vigilante de Esc. Pero solo si de verdad se
|
|
855
|
+
// va a preguntar: pararlo en cada herramienta dejaria el resto del
|
|
856
|
+
// lote sin Esc, que es justo lo que no se quiere.
|
|
857
|
+
this.apuntarEscritura(fnName, fnArgs, result);
|
|
858
|
+
const texto = await this.vigilarBucle(fnName, tc.function.arguments, result, () => watcher.stop());
|
|
456
859
|
toolResults.push({
|
|
457
860
|
role: "tool",
|
|
458
861
|
tool_call_id: tc.id,
|
|
459
|
-
content: post.additionalContext ? `${
|
|
460
|
-
[Hook PostToolUse]: ${post.additionalContext}` :
|
|
862
|
+
content: post.additionalContext ? `${texto}\n\n
|
|
863
|
+
[Hook PostToolUse]: ${post.additionalContext}` : texto,
|
|
461
864
|
});
|
|
462
865
|
}
|
|
463
866
|
// Push all tool results in a single turn
|
|
@@ -465,7 +868,7 @@ export class AgentLoop {
|
|
|
465
868
|
watcher.stop();
|
|
466
869
|
if (watcher.interrupted) {
|
|
467
870
|
console.log(chalk.yellow("\n⏸ Interrumpido. Dime como seguir."));
|
|
468
|
-
return;
|
|
871
|
+
return parada("interrumpido");
|
|
469
872
|
}
|
|
470
873
|
continue;
|
|
471
874
|
}
|
|
@@ -478,27 +881,82 @@ export class AgentLoop {
|
|
|
478
881
|
content,
|
|
479
882
|
});
|
|
480
883
|
}
|
|
884
|
+
// El modelo da el turno por cerrado. Si toco archivos y el proyecto tiene
|
|
885
|
+
// verificacion automatica activada, se comprueba ANTES de creerselo: un
|
|
886
|
+
// error de tipos que nadie mira se queda en el disco y los turnos
|
|
887
|
+
// siguientes escriben encima.
|
|
888
|
+
const pendiente = await this.verificarTrasEditar(reflexiones);
|
|
889
|
+
if (pendiente) {
|
|
890
|
+
reflexiones++;
|
|
891
|
+
this.messages.push({ role: "user", content: pendiente });
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
completado = true;
|
|
481
895
|
break;
|
|
482
896
|
}
|
|
483
897
|
const durationMs = Date.now() - startTime;
|
|
484
898
|
const { formattedCost, totalCost } = this.tracker.calculateCost(turnPromptTokens, turnCachedTokens, turnCompletionTokens, this.currentEspectro.underlyingModel);
|
|
485
899
|
const approxTotalTokens = turnPromptTokens + turnCompletionTokens;
|
|
486
|
-
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
|
|
900
|
+
// Lo que ocupa el historial es el prompt de la ultima llamada. Sumar el de
|
|
901
|
+
// todas las iteraciones inflaba la cuenta hasta disparar la compactacion
|
|
902
|
+
// con el contexto medio vacio, y hacia mentir al porcentaje del pie.
|
|
903
|
+
this.lastTurnTokens = lastCallPromptTokens || approxTotalTokens;
|
|
904
|
+
const used = lastCallPromptTokens || turnPromptTokens || approxTotalTokens;
|
|
490
905
|
const contextPercent = Math.max(1, 100 - Math.round((used / CONTEXT_WINDOW) * 100));
|
|
491
|
-
|
|
492
|
-
//
|
|
906
|
+
// Se apunta lo gastado en este turno para las ventanas deslizantes. El
|
|
907
|
+
// checkpoint de /undo ya no se cierra aqui, sino en parada(): este punto
|
|
908
|
+
// solo lo alcanzaban los turnos que terminaban limpios.
|
|
493
909
|
recordUsage(totalCost);
|
|
910
|
+
// Los subagentes, la vision y la compactacion ya se apuntaron solos segun
|
|
911
|
+
// gastaban. Se dice en voz alta porque si no el pie parece contradecirse:
|
|
912
|
+
// marca 0,0012 $ de turno y la insignia semanal sube diez veces mas.
|
|
913
|
+
const auxiliar = tomarGastoAuxiliar();
|
|
494
914
|
const tras = checkQuota();
|
|
495
915
|
renderFooter(formattedCost, turnCompletionTokens, turnCachedTokens, this.currentEspectro.name, durationMs, contextPercent, quotaBadge(tras));
|
|
916
|
+
if (auxiliar.total > 0) {
|
|
917
|
+
console.log(chalk.gray(` + $${auxiliar.total.toFixed(4)} USD fuera del bucle (${auxiliar.rutas}), ya contados en la cuota.\n`));
|
|
918
|
+
}
|
|
496
919
|
// Aviso al acercarse, no solo al chocar: quedarse a medias sin previo aviso
|
|
497
920
|
// es lo que hace que la gente cancele.
|
|
498
921
|
if (tras.allowed && tras.spentWeekUsd >= tras.config.weeklyBudgetUsd * 0.8) {
|
|
499
922
|
const queda = Math.max(0, tras.config.weeklyBudgetUsd - tras.spentWeekUsd);
|
|
500
923
|
console.log(chalk.yellow(` ⚠ Te queda poca cuota semanal (${((queda / tras.config.weeklyBudgetUsd) * 100).toFixed(0)}%).\n`));
|
|
501
924
|
}
|
|
925
|
+
if (!completado) {
|
|
926
|
+
// Se agotaron las iteraciones con trabajo a medias. En interactivo se ve
|
|
927
|
+
// porque el ultimo mensaje queda cortado; en CI no lo ve nadie.
|
|
928
|
+
const aviso = `El turno se agoto en el limite de ${this.maxIterations} iteraciones sin cerrar la tarea.`;
|
|
929
|
+
console.log(chalk.yellow(` ⚠ ${aviso}\n`));
|
|
930
|
+
return parada("limite-iteraciones", aviso);
|
|
931
|
+
}
|
|
932
|
+
return parada("completado");
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Guarda lo que el usuario acaba de aprobar con "no preguntar mas".
|
|
936
|
+
*
|
|
937
|
+
* Aqui se hacia command.split(" ")[0] y se guardaba solo el binario. Aprobar
|
|
938
|
+
* un "git status" dejaba el patron "git" escrito en .chocolatito/settings.json
|
|
939
|
+
* y, como el allow autoriza por prefijo, a partir de ese momento pasaban sin
|
|
940
|
+
* dialogo "git push --force" y "git reset --hard", en esa sesion y en todas
|
|
941
|
+
* las siguientes. Un solo si de mas y el proyecto se queda sin barrera.
|
|
942
|
+
*
|
|
943
|
+
* Ahora se guarda la firma del comando (binario mas subcomando) y, si el
|
|
944
|
+
* comando es de los destructivos, no se guarda nada: eso se pregunta siempre,
|
|
945
|
+
* y decirlo en voz alta es mejor que escribir un patron que no serviria.
|
|
946
|
+
*/
|
|
947
|
+
recordarPermiso(fnName, fnArgs) {
|
|
948
|
+
const command = typeof fnArgs?.command === "string" ? fnArgs.command : "";
|
|
949
|
+
if (command) {
|
|
950
|
+
if (isDangerousCommand(command)) {
|
|
951
|
+
console.log(chalk.gray(" (este comando es destructivo: se seguira preguntando cada vez, no se puede recordar)\n"));
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
const firma = commandSignature(command);
|
|
955
|
+
if (firma)
|
|
956
|
+
this.permissions.saveAllowPattern(`${fnName}:${firma}`);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
this.permissions.saveAllowPattern(fnArgs?.path || fnName);
|
|
502
960
|
}
|
|
503
961
|
}
|
|
504
962
|
function getToolSummary(name, args) {
|