chocolatito-code 1.1.0 → 1.3.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 +66 -0
- package/dist/agent/huecos.d.ts +58 -0
- package/dist/agent/huecos.js +81 -0
- package/dist/agent/loop.js +218 -41
- package/dist/agent/salidaDelAgente.d.ts +43 -0
- package/dist/agent/salidaDelAgente.js +39 -0
- package/dist/agent/toolGate.js +6 -1
- package/dist/config/engine.d.ts +21 -1
- package/dist/config/engine.js +22 -1
- package/dist/config/nodeVersion.d.ts +13 -0
- package/dist/config/nodeVersion.js +26 -0
- package/dist/index.js +22 -7
- package/dist/mcp/client.d.ts +28 -0
- package/dist/mcp/client.js +62 -8
- package/dist/mcp/ide.d.ts +76 -0
- package/dist/mcp/ide.js +160 -0
- package/dist/mcp/manager.js +9 -0
- package/dist/tools/browserExtension.js +153 -7
- package/dist/tools/computerUse.d.ts +1 -1
- package/dist/tools/computerUse.js +183 -2
- package/dist/tools/definitions.js +9 -2
- package/dist/tools/grepSearch.js +111 -23
- package/dist/tools/runner.js +1 -1
- package/dist/tools/toolDefsComputer.js +6 -2
- package/dist/tools/visionBridge.js +60 -3
- package/dist/tools/win/hostScript.js +154 -0
- package/dist/tools/writeFile.d.ts +16 -1
- package/dist/tools/writeFile.js +22 -1
- package/dist/ui/interrupt.d.ts +10 -0
- package/dist/ui/interrupt.js +53 -4
- package/dist/ui/keyboardGuard.d.ts +21 -0
- package/dist/ui/keyboardGuard.js +30 -1
- package/dist/ui/pegados.d.ts +50 -0
- package/dist/ui/pegados.js +83 -0
- package/dist/ui/pieFijo.d.ts +56 -0
- package/dist/ui/pieFijo.js +309 -0
- package/dist/ui/prompt.d.ts +5 -0
- package/dist/ui/prompt.js +109 -16
- package/dist/ui/reasoningStream.js +22 -12
- package/dist/ui/salida.d.ts +10 -0
- package/dist/ui/salida.js +50 -0
- package/dist/ui/spinner.d.ts +15 -8
- package/dist/ui/spinner.js +80 -42
- package/extension/background.js +191 -14
- package/extension/content.js +446 -48
- package/extension/fuentes/Chocolatito-Marca.ttf +0 -0
- package/extension/iconos/128.png +0 -0
- package/extension/iconos/16.png +0 -0
- package/extension/iconos/32.png +0 -0
- package/extension/iconos/48.png +0 -0
- package/extension/iconos/ORIGEN.md +48 -0
- package/extension/iconos/logotipo.png +0 -0
- package/extension/manifest.json +27 -4
- package/extension/popup.html +16 -6
- package/package.json +3 -2
package/dist/agent/loop.js
CHANGED
|
@@ -5,9 +5,10 @@ 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, isContextOverflow } from "./retry.js";
|
|
8
|
+
import { withRetry, isContextOverflow, esDemasiadasEnCurso } from "./retry.js";
|
|
9
9
|
import { canRunInParallel } from "../tools/safety.js";
|
|
10
10
|
import { createEngineClient, explicarErrorDelMotor, motivoDeParadaDelError, } from "../config/engine.js";
|
|
11
|
+
import { anotarHuecosOcupados, liberarHuecos, mensajeDeEspera, segundosDeEspera } from "./huecos.js";
|
|
11
12
|
import { MarkdownStream } from "../ui/markdownStream.js";
|
|
12
13
|
import { checkQuota, recordUsage, mensajeDeCorte, quotaBadge } from "../config/quota.js";
|
|
13
14
|
import { ReasoningStream } from "../ui/reasoningStream.js";
|
|
@@ -15,6 +16,7 @@ import { PermissionManager, commandSignature, isDangerousCommand } from "../conf
|
|
|
15
16
|
import { askToolPermission } from "../ui/permissionPrompt.js";
|
|
16
17
|
import { DynamicSpinner } from "../ui/spinner.js";
|
|
17
18
|
import { globalUndoManager } from "./undoManager.js";
|
|
19
|
+
import { salida } from "./salidaDelAgente.js";
|
|
18
20
|
import { InterruptWatcher, isAbortError } from "../ui/interrupt.js";
|
|
19
21
|
import { HookManager } from "../hooks/manager.js";
|
|
20
22
|
import { registerToolGate, autorizarHerramienta, notificarPostHerramienta } from "./toolGate.js";
|
|
@@ -33,6 +35,15 @@ const HERRAMIENTAS_QUE_ESCRIBEN = new Set([
|
|
|
33
35
|
]);
|
|
34
36
|
/** Cortes por longitud seguidos que se toleran antes de rendirse. */
|
|
35
37
|
const MAX_CORTES_POR_LONGITUD = 2;
|
|
38
|
+
/**
|
|
39
|
+
* Respuestas vacias seguidas que se toleran antes de rendirse.
|
|
40
|
+
*
|
|
41
|
+
* Dos, no una: una respuesta vacia suelta es casi siempre un hipo del motor y
|
|
42
|
+
* el reintento la resuelve sin que el usuario se entere. Tres seguidas ya no es
|
|
43
|
+
* un hipo, y seguir reintentando en silencio es lo que hacia que pareciera que
|
|
44
|
+
* el agente te ignora.
|
|
45
|
+
*/
|
|
46
|
+
const MAX_RESPUESTAS_VACIAS = 1;
|
|
36
47
|
/** Mensajes recientes que la compactacion no resume nunca. */
|
|
37
48
|
const COLA_INTACTA = 4;
|
|
38
49
|
/**
|
|
@@ -66,8 +77,43 @@ function parsearArgumentos(bruto) {
|
|
|
66
77
|
/** Lo que se le dice al modelo cuando sus argumentos llegaron a medias. */
|
|
67
78
|
function mensajeDeArgumentosRotos(herramienta) {
|
|
68
79
|
return (`No se ejecuto "${herramienta}": los argumentos llegaron incompletos, seguramente porque la ` +
|
|
69
|
-
`respuesta se corto por longitud. No se ha hecho nada.
|
|
70
|
-
|
|
80
|
+
`respuesta se corto por longitud. No se ha hecho nada.
|
|
81
|
+
|
|
82
|
+
` +
|
|
83
|
+
`Si era un archivo largo, hazlo por partes: una primera llamada a write_file con el principio ` +
|
|
84
|
+
`y las siguientes con modo="anadir" para ir pegando el resto. Cada trozo, por debajo de unas ` +
|
|
85
|
+
`400 lineas.`);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Deja una llamada a herramienta en un estado que la API acepte.
|
|
89
|
+
*
|
|
90
|
+
* EL FALLO QUE CIERRA ESTO
|
|
91
|
+
*
|
|
92
|
+
* Cuando la respuesta se corta a mitad de un `write_file` con un HTML largo
|
|
93
|
+
* dentro, los argumentos llegan asi:
|
|
94
|
+
*
|
|
95
|
+
* {"path":"index.html","content":"<!doctype html><html><head><style>body{ma
|
|
96
|
+
*
|
|
97
|
+
* La herramienta ya no se ejecutaba -eso se arreglo antes-, pero la llamada se
|
|
98
|
+
* guardaba en el historial TAL CUAL, y el historial se reenvia entero en cada
|
|
99
|
+
* peticion. La API valida que los argumentos sean JSON, no lo son, y contesta
|
|
100
|
+
* 400. Desde ese momento la sesion queda inservible: todo lo que escriba el
|
|
101
|
+
* usuario falla igual, la unica salida es /clear, y nadie lo sabe.
|
|
102
|
+
*
|
|
103
|
+
* Se vio en una sesion real, y encima el 400 se llevo por delante los cuatro
|
|
104
|
+
* huecos de peticion de la licencia, asi que al fallo se le sumo un "Tienes 4
|
|
105
|
+
* peticiones en curso" de dos minutos y medio.
|
|
106
|
+
*
|
|
107
|
+
* Guardando "{}" el historial sigue siendo valido y la pareja llamada/respuesta
|
|
108
|
+
* cuadra. Al modelo no se le oculta nada: el resultado de esa herramienta ya le
|
|
109
|
+
* dice que sus argumentos llegaron a medias y que no se hizo nada.
|
|
110
|
+
*/
|
|
111
|
+
function sanearLlamada(tc) {
|
|
112
|
+
const bruto = tc?.function?.arguments;
|
|
113
|
+
const texto = typeof bruto === "string" ? bruto.trim() : "";
|
|
114
|
+
if (texto && parsearArgumentos(texto).ok)
|
|
115
|
+
return tc;
|
|
116
|
+
return { ...tc, function: { ...tc.function, arguments: "{}" } };
|
|
71
117
|
}
|
|
72
118
|
export class AgentLoop {
|
|
73
119
|
client;
|
|
@@ -223,7 +269,7 @@ export class AgentLoop {
|
|
|
223
269
|
const proyectado = antes > 0 ? Math.round(tokensActuales * (despues / antes)) : tokensActuales;
|
|
224
270
|
this.lastTurnTokens = proyectado;
|
|
225
271
|
if (proyectado < COMPACT_THRESHOLD_TOKENS * 0.8) {
|
|
226
|
-
|
|
272
|
+
salida.linea(chalk.hex("#D97757")("\n⚡ [Auto-Compact]: ") +
|
|
227
273
|
chalk.gray(`Liberados ~${(antes - despues).toLocaleString()} tokens de salidas de herramientas antiguas. ` +
|
|
228
274
|
"La conversacion se conserva entera.\n"));
|
|
229
275
|
return true;
|
|
@@ -278,7 +324,7 @@ export class AgentLoop {
|
|
|
278
324
|
// Queda una estimacion hasta la siguiente respuesta de la API: mejor eso
|
|
279
325
|
// que arrastrar la cifra vieja, que dispararia otra compactacion seguida.
|
|
280
326
|
this.lastTurnTokens = estimarTokens(this.messages);
|
|
281
|
-
|
|
327
|
+
salida.linea(chalk.hex("#D97757")("\n⚡ [Auto-Compact]: ") +
|
|
282
328
|
chalk.gray(`Contexto optimizado automáticamente (${this.messages.length} mensajes limpios) para mantener alta velocidad y mínimo consumo de API.\n`));
|
|
283
329
|
return true;
|
|
284
330
|
}
|
|
@@ -286,7 +332,7 @@ export class AgentLoop {
|
|
|
286
332
|
spinner.stop();
|
|
287
333
|
// El catch mudo era medio fallo: si el resumen no sale, el turno sigue con
|
|
288
334
|
// el contexto entero y el error de despues llega sin explicacion.
|
|
289
|
-
|
|
335
|
+
salida.linea(chalk.gray(` (no se pudo compactar el contexto: ${err?.message || String(err)})`));
|
|
290
336
|
return false;
|
|
291
337
|
}
|
|
292
338
|
}
|
|
@@ -307,7 +353,7 @@ export class AgentLoop {
|
|
|
307
353
|
if (!cmd)
|
|
308
354
|
return null;
|
|
309
355
|
if (reflexiones >= MAX_REFLEXIONES) {
|
|
310
|
-
|
|
356
|
+
salida.linea(chalk.yellow(`\n⚠ ${mensajeDeRendicion(cmd.comando)}\n`));
|
|
311
357
|
return null;
|
|
312
358
|
}
|
|
313
359
|
const spinner = new DynamicSpinner();
|
|
@@ -320,13 +366,13 @@ export class AgentLoop {
|
|
|
320
366
|
spinner.stop();
|
|
321
367
|
}
|
|
322
368
|
if (resultado.ok) {
|
|
323
|
-
|
|
369
|
+
salida.linea(chalk.gray(` ✔ Verificado con ${cmd.comando}\n`));
|
|
324
370
|
// Lo verificado deja de estar pendiente: si el modelo vuelve a editar,
|
|
325
371
|
// se vuelve a comprobar; si no, no se comprueba dos veces lo mismo.
|
|
326
372
|
this.archivosTocados.clear();
|
|
327
373
|
return null;
|
|
328
374
|
}
|
|
329
|
-
|
|
375
|
+
salida.linea(chalk.yellow(`\n⚠ La verificacion fallo (${cmd.comando}). Se lo devuelvo al agente.\n`));
|
|
330
376
|
return mensajeDeVerificacion(cmd.comando, resultado.salida, reflexiones + 1);
|
|
331
377
|
}
|
|
332
378
|
/**
|
|
@@ -410,12 +456,21 @@ export class AgentLoop {
|
|
|
410
456
|
tokensSalida: turnCompletionTokens,
|
|
411
457
|
};
|
|
412
458
|
};
|
|
459
|
+
// Los huecos de peticion, ANTES de gastar uno. El proxy ya nos dijo cuanto
|
|
460
|
+
// falta; volver a preguntarselo no puede salir bien por definicion, y esa
|
|
461
|
+
// llamada es justo la que puede refrescar la reserva y alargar la espera.
|
|
462
|
+
const espera = segundosDeEspera();
|
|
463
|
+
if (espera > 0) {
|
|
464
|
+
const aviso = mensajeDeEspera(espera);
|
|
465
|
+
salida.linea(chalk.yellow(`\n⏸ ${aviso}\n`));
|
|
466
|
+
return parada("ocupado", aviso);
|
|
467
|
+
}
|
|
413
468
|
// Cuota ANTES de gastar. Si no queda margen, se dice y no se llama al motor:
|
|
414
469
|
// avisar despues de haber gastado no sirve de nada.
|
|
415
470
|
const cuota = checkQuota();
|
|
416
471
|
if (!cuota.allowed) {
|
|
417
472
|
const corte = mensajeDeCorte(cuota);
|
|
418
|
-
|
|
473
|
+
salida.linea(chalk.yellow(`\n⏸ ${corte}`) +
|
|
419
474
|
chalk.gray("\n Tu trabajo y tu contexto siguen aqui: al liberarse, continuas donde lo dejaste.\n"));
|
|
420
475
|
return parada("cuota", corte);
|
|
421
476
|
}
|
|
@@ -438,6 +493,8 @@ export class AgentLoop {
|
|
|
438
493
|
let reflexiones = 0;
|
|
439
494
|
/** Veces seguidas que la respuesta se corto por longitud sin hacer nada. */
|
|
440
495
|
let cortesPorLongitud = 0;
|
|
496
|
+
/** Veces seguidas que el turno acabo sin una sola letra que ensenar. */
|
|
497
|
+
let respuestasVacias = 0;
|
|
441
498
|
// Se limpia aqui: lo que se edito en el turno anterior ya se verifico (o no
|
|
442
499
|
// se iba a verificar nunca).
|
|
443
500
|
this.archivosTocados.clear();
|
|
@@ -479,7 +536,7 @@ export class AgentLoop {
|
|
|
479
536
|
}, { signal }), {
|
|
480
537
|
onRetry: (attempt, delay, reason) => {
|
|
481
538
|
spinner.stop();
|
|
482
|
-
|
|
539
|
+
salida.linea(chalk.yellow(`\n⟳ El motor respondio ${reason}. Reintento ${attempt} en ${(delay / 1000).toFixed(1)}s...`));
|
|
483
540
|
spinner.start();
|
|
484
541
|
},
|
|
485
542
|
});
|
|
@@ -488,7 +545,7 @@ export class AgentLoop {
|
|
|
488
545
|
spinner.stop();
|
|
489
546
|
watcher.stop();
|
|
490
547
|
if (isAbortError(err) || watcher.interrupted) {
|
|
491
|
-
|
|
548
|
+
salida.linea(chalk.yellow("\n⏸ Interrumpido. Dime como seguir."));
|
|
492
549
|
return parada("interrumpido");
|
|
493
550
|
}
|
|
494
551
|
// El contexto no cabe. Reintentar la misma peticion es imposible por
|
|
@@ -497,7 +554,7 @@ export class AgentLoop {
|
|
|
497
554
|
// sola vez: si tras compactar sigue sin caber, el problema es otro.
|
|
498
555
|
if (isContextOverflow(err) && !yaCompactadoPorDesbordamiento) {
|
|
499
556
|
yaCompactadoPorDesbordamiento = true;
|
|
500
|
-
|
|
557
|
+
salida.linea(chalk.yellow("\n⚠ La peticion no cabia en el contexto. Compactando y reintentando..."));
|
|
501
558
|
const activo = this.messages[0]?.role === "system" ? this.messages[0].content : "";
|
|
502
559
|
await this.compactHistory(activo);
|
|
503
560
|
iterations--; // este intento no llego al modelo: no cuenta como vuelta
|
|
@@ -506,9 +563,14 @@ export class AgentLoop {
|
|
|
506
563
|
// Un 402 (cuota agotada) o un 401 (licencia caida) no son "fallos del
|
|
507
564
|
// motor" y ademas no se reintentan nunca: decirlo como tal confunde y
|
|
508
565
|
// hace que el usuario piense que el producto esta roto.
|
|
566
|
+
// Si el 429 es de huecos ocupados, se apunta hasta cuando: asi el
|
|
567
|
+
// siguiente mensaje del usuario se responde desde aqui en vez de gastar
|
|
568
|
+
// otra peticion para que le repitan la misma pared.
|
|
569
|
+
if (esDemasiadasEnCurso(err))
|
|
570
|
+
anotarHuecosOcupados(err?.error?.retry_after_s);
|
|
509
571
|
const claro = explicarErrorDelMotor(err);
|
|
510
572
|
if (claro) {
|
|
511
|
-
|
|
573
|
+
salida.linea(chalk.yellow(`
|
|
512
574
|
⏸ ${claro}
|
|
513
575
|
`));
|
|
514
576
|
return parada(motivoDeParadaDelError(err), claro);
|
|
@@ -517,19 +579,34 @@ export class AgentLoop {
|
|
|
517
579
|
renderError(fallo);
|
|
518
580
|
return parada("error-motor", fallo);
|
|
519
581
|
}
|
|
582
|
+
// El motor contesto: habia hueco. Seguir bloqueando por una espera vieja
|
|
583
|
+
// seria mentirle al usuario y negarle el servicio que si tiene.
|
|
584
|
+
liberarHuecos();
|
|
520
585
|
let content = "";
|
|
521
586
|
let reasoning = "";
|
|
522
587
|
let motivoDeCorte = null;
|
|
588
|
+
/**
|
|
589
|
+
* Si llego la MARCA de final del stream.
|
|
590
|
+
*
|
|
591
|
+
* Un stream sano termina diciendo por que termina: un `finish_reason` y,
|
|
592
|
+
* con `include_usage`, un ultimo trozo con el gasto. Cuando el cuerpo se
|
|
593
|
+
* corta a mitad no llega ninguno de los dos y el iterador se acaba sin
|
|
594
|
+
* error, exactamente igual que si hubiera terminado bien. Hacen falta las
|
|
595
|
+
* dos ausencias: fiarse de una sola convertiria en "cortada" cualquier
|
|
596
|
+
* respuesta de un motor que no mande esa marca.
|
|
597
|
+
*/
|
|
598
|
+
let llegoElFinal = false;
|
|
523
599
|
const rawToolCalls = [];
|
|
524
|
-
let isFirstChunk = true;
|
|
525
600
|
const reasoningStream = new ReasoningStream();
|
|
526
|
-
|
|
601
|
+
// Por `salida`, no por stdout: mientras se escribe la respuesta el spinner
|
|
602
|
+
// sigue puesto abajo, y escribir directo lo pisaria.
|
|
603
|
+
const markdown = new MarkdownStream((s) => salida.crudo(s));
|
|
527
604
|
try {
|
|
528
605
|
for await (const chunk of stream) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
606
|
+
// El spinner ya NO se para al llegar el primer trozo. Vive en el pie
|
|
607
|
+
// fijo, asi que la respuesta sale por encima sin taparlo y el contador
|
|
608
|
+
// de segundos sigue a la vista mientras el modelo habla, que es
|
|
609
|
+
// exactamente cuando uno quiere saber cuanto lleva.
|
|
533
610
|
const delta = chunk.choices?.[0]?.delta;
|
|
534
611
|
// Handle real-time reasoning stream
|
|
535
612
|
if (delta?.reasoning_content) {
|
|
@@ -543,6 +620,12 @@ export class AgentLoop {
|
|
|
543
620
|
content += delta.content;
|
|
544
621
|
markdown.push(delta.content);
|
|
545
622
|
}
|
|
623
|
+
// El contador del spinner, en vivo. El `usage` de verdad solo llega
|
|
624
|
+
// en el ultimo trozo: sin esta estimacion el numero se queda a cero
|
|
625
|
+
// durante toda la respuesta y aparece de golpe justo al acabar, que
|
|
626
|
+
// es cuando ya no le sirve a nadie.
|
|
627
|
+
spinner.tokenCount =
|
|
628
|
+
turnCompletionTokens + Math.round((content.length + reasoning.length) / 4);
|
|
546
629
|
// Accumulate fragmented tool calls by index tc.index
|
|
547
630
|
if (delta?.tool_calls) {
|
|
548
631
|
for (const tc of delta.tool_calls) {
|
|
@@ -565,10 +648,13 @@ export class AgentLoop {
|
|
|
565
648
|
// El motivo por el que el modelo dejo de hablar. Sin esto, quedarse
|
|
566
649
|
// sin presupuesto de salida es indistinguible de terminar el turno.
|
|
567
650
|
const corte = chunk.choices?.[0]?.finish_reason;
|
|
568
|
-
if (corte)
|
|
651
|
+
if (corte) {
|
|
569
652
|
motivoDeCorte = corte;
|
|
653
|
+
llegoElFinal = true;
|
|
654
|
+
}
|
|
570
655
|
// Track streaming usage metrics
|
|
571
656
|
if (chunk.usage) {
|
|
657
|
+
llegoElFinal = true;
|
|
572
658
|
this.tracker.addUsage(chunk.usage);
|
|
573
659
|
turnPromptTokens += chunk.usage.prompt_tokens || 0;
|
|
574
660
|
if (chunk.usage.prompt_tokens)
|
|
@@ -589,12 +675,14 @@ export class AgentLoop {
|
|
|
589
675
|
if (content.trim()) {
|
|
590
676
|
this.messages.push({ role: "assistant", content });
|
|
591
677
|
}
|
|
592
|
-
|
|
678
|
+
salida.linea(chalk.yellow("\n⏸ Interrumpido. Dime como seguir."));
|
|
593
679
|
return parada("interrumpido");
|
|
594
680
|
}
|
|
681
|
+
if (esDemasiadasEnCurso(streamErr))
|
|
682
|
+
anotarHuecosOcupados(streamErr?.error?.retry_after_s);
|
|
595
683
|
const claroStream = explicarErrorDelMotor(streamErr);
|
|
596
684
|
if (claroStream) {
|
|
597
|
-
|
|
685
|
+
salida.linea(chalk.yellow(`
|
|
598
686
|
⏸ ${claroStream}
|
|
599
687
|
`));
|
|
600
688
|
return parada(motivoDeParadaDelError(streamErr), claroStream);
|
|
@@ -610,11 +698,54 @@ export class AgentLoop {
|
|
|
610
698
|
watcher.stop();
|
|
611
699
|
if (content.trim())
|
|
612
700
|
this.messages.push({ role: "assistant", content });
|
|
613
|
-
|
|
701
|
+
salida.linea(chalk.yellow("\n⏸ Interrumpido antes de ejecutar las herramientas. Dime como seguir."));
|
|
614
702
|
return parada("interrumpido");
|
|
615
703
|
}
|
|
616
704
|
// Filter valid accumulated tool calls
|
|
617
705
|
const toolCalls = rawToolCalls.filter((tc) => tc && tc.function && tc.function.name);
|
|
706
|
+
// LA RESPUESTA SE CORTO A MITAD Y EL STREAM ACABO COMO SI NADA
|
|
707
|
+
//
|
|
708
|
+
// Sesion real. El usuario pregunta por su consumo y ve esto:
|
|
709
|
+
//
|
|
710
|
+
// ¿De cuál consumo me hab
|
|
711
|
+
// ⚡ 12.4s · 0 tokens salida · 100% contexto libre
|
|
712
|
+
//
|
|
713
|
+
// Media palabra y el pie del turno, sin un solo aviso. El cuerpo de la
|
|
714
|
+
// respuesta se cerro por el camino -no el modelo: la conexion-, asi que no
|
|
715
|
+
// llego ni el finish_reason ni el trozo del gasto, y el iterador termino
|
|
716
|
+
// sin lanzar nada. El bucle no distingue "el modelo termino" de "no vino
|
|
717
|
+
// mas", asi que dio el turno por bueno.
|
|
718
|
+
//
|
|
719
|
+
// Y lo peor no se veia: esa media frase se guardaba en el historial como
|
|
720
|
+
// respuesta completa del asistente. El turno siguiente salia con un
|
|
721
|
+
// historial que acaba a mitad de palabra, el modelo contestaba vacio, y el
|
|
722
|
+
// reintento de las respuestas vacias gastaba otro hueco de peticion.
|
|
723
|
+
// Cuatro mensajes despues: "Tienes 4 peticiones en curso. Reintenta en
|
|
724
|
+
// 197s", y el usuario sin servicio durante tres minutos.
|
|
725
|
+
//
|
|
726
|
+
// NO SE REINTENTA, A PROPOSITO. Cada intento ocupa uno de los cuatro
|
|
727
|
+
// huecos de la licencia durante minutos, y los cortes vienen en rachas:
|
|
728
|
+
// reintentar es justo como se pasa de perder un hueco a perder los cuatro.
|
|
729
|
+
// Mas vale que el usuario reescriba una linea.
|
|
730
|
+
if (!llegoElFinal && (content.trim().length > 0 || toolCalls.length > 0)) {
|
|
731
|
+
watcher.stop();
|
|
732
|
+
// Lo que llego se guarda, pero MARCADO. Sin la marca, en el turno
|
|
733
|
+
// siguiente el modelo lee su propia frase a medias como algo que decidio
|
|
734
|
+
// dejar asi, y responde a un texto que nadie escribio.
|
|
735
|
+
if (content.trim()) {
|
|
736
|
+
this.messages.push({
|
|
737
|
+
role: "assistant",
|
|
738
|
+
content: `${content}\n\n[Aqui se corto: la conexion con el motor se cerro a mitad de la respuesta.]`,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
// Las llamadas a herramienta de un stream cortado NO se ejecutan: sus
|
|
742
|
+
// argumentos estan tan a medias como el texto.
|
|
743
|
+
const aviso = "La respuesta se corto a mitad: la conexion con el motor se cerro sin avisar. " +
|
|
744
|
+
"Lo que llego se queda arriba. No lo reintento solo porque cada intento ocupa uno de " +
|
|
745
|
+
"los cuatro huecos de tu licencia durante varios minutos; vuelve a pedirmelo y sigo.";
|
|
746
|
+
salida.linea(chalk.yellow(`\n ⚠ ${aviso}\n`));
|
|
747
|
+
return parada("error-motor", aviso);
|
|
748
|
+
}
|
|
618
749
|
// El modelo se quedo sin presupuesto de salida y no llego a pedir nada.
|
|
619
750
|
//
|
|
620
751
|
// Esto se veia EXACTAMENTE igual que un turno terminado: sin llamadas a
|
|
@@ -630,19 +761,20 @@ export class AgentLoop {
|
|
|
630
761
|
if (cortesPorLongitud > MAX_CORTES_POR_LONGITUD) {
|
|
631
762
|
const aviso = `La respuesta se corto por longitud ${cortesPorLongitud} veces seguidas sin llegar a ` +
|
|
632
763
|
`hacer nada. Prueba a pedirlo por partes.`;
|
|
633
|
-
|
|
764
|
+
salida.linea(chalk.yellow(`\n ⚠ ${aviso}\n`));
|
|
634
765
|
if (content.trim())
|
|
635
766
|
this.messages.push({ role: "assistant", content });
|
|
636
767
|
return parada("limite-iteraciones", aviso);
|
|
637
768
|
}
|
|
638
|
-
|
|
769
|
+
salida.linea(chalk.yellow("\n ⚠ La respuesta se corto por longitud antes de hacer nada. Pidiendole que vaya al grano...\n"));
|
|
639
770
|
if (content.trim())
|
|
640
771
|
this.messages.push({ role: "assistant", content });
|
|
641
772
|
this.messages.push({
|
|
642
773
|
role: "user",
|
|
643
774
|
content: "Tu respuesta anterior se corto porque agoto el presupuesto de salida mientras razonabas, " +
|
|
644
775
|
"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,
|
|
776
|
+
"y actua ya. Si lo que hay que escribir es largo, empieza con write_file y sigue con " +
|
|
777
|
+
'write_file en modo="anadir", en trozos de menos de 400 lineas.',
|
|
646
778
|
});
|
|
647
779
|
// Sin esto se queda un vigilante de teclado vivo por cada corte, y cada
|
|
648
780
|
// uno vuelve a apuntarse como oyente: la misma tecla se procesa tantas
|
|
@@ -655,7 +787,10 @@ export class AgentLoop {
|
|
|
655
787
|
this.messages.push({
|
|
656
788
|
role: "assistant",
|
|
657
789
|
content: content || null,
|
|
658
|
-
|
|
790
|
+
// Sanear NO es cosmetico: unos argumentos cortados guardados tal cual
|
|
791
|
+
// hacen que la API conteste 400 en TODAS las peticiones siguientes, y
|
|
792
|
+
// la sesion queda inservible hasta un /clear. Ver sanearLlamada().
|
|
793
|
+
tool_calls: toolCalls.map(sanearLlamada),
|
|
659
794
|
});
|
|
660
795
|
const toolResults = [];
|
|
661
796
|
// Las llamadas independientes de solo lectura se lanzan a la vez. Cuando
|
|
@@ -733,7 +868,7 @@ export class AgentLoop {
|
|
|
733
868
|
toolResults.forEach((r) => this.messages.push(r));
|
|
734
869
|
watcher.stop();
|
|
735
870
|
if (watcher.interrupted) {
|
|
736
|
-
|
|
871
|
+
salida.linea(chalk.yellow("\n⏸ Interrumpido. Dime como seguir."));
|
|
737
872
|
return parada("interrumpido");
|
|
738
873
|
}
|
|
739
874
|
continue;
|
|
@@ -781,7 +916,7 @@ export class AgentLoop {
|
|
|
781
916
|
fnArgs = { ...fnArgs, ...pre.updatedInput };
|
|
782
917
|
}
|
|
783
918
|
if (pre.systemMessage) {
|
|
784
|
-
|
|
919
|
+
salida.linea(chalk.yellow(`\n⚑ ${pre.systemMessage}`));
|
|
785
920
|
}
|
|
786
921
|
// Modo plan: bloqueo duro, no un dialogo. Lo que el modo promete es
|
|
787
922
|
// solo lectura, asi que aqui no hay nada que preguntar.
|
|
@@ -848,7 +983,7 @@ export class AgentLoop {
|
|
|
848
983
|
// El systemMessage de PreToolUse si se imprimia y el de PostToolUse
|
|
849
984
|
// no: el mismo campo, del mismo hook, se veia o no segun el evento.
|
|
850
985
|
if (post.systemMessage) {
|
|
851
|
-
|
|
986
|
+
salida.linea(chalk.yellow(`\n⚑ ${post.systemMessage}`));
|
|
852
987
|
}
|
|
853
988
|
// El dialogo del bucle usa stdin, igual que el de permisos: hay que
|
|
854
989
|
// soltarle el terminal al vigilante de Esc. Pero solo si de verdad se
|
|
@@ -867,20 +1002,62 @@ export class AgentLoop {
|
|
|
867
1002
|
toolResults.forEach((r) => this.messages.push(r));
|
|
868
1003
|
watcher.stop();
|
|
869
1004
|
if (watcher.interrupted) {
|
|
870
|
-
|
|
1005
|
+
salida.linea(chalk.yellow("\n⏸ Interrumpido. Dime como seguir."));
|
|
871
1006
|
return parada("interrumpido");
|
|
872
1007
|
}
|
|
873
1008
|
continue;
|
|
874
1009
|
}
|
|
875
1010
|
watcher.stop();
|
|
876
|
-
//
|
|
877
|
-
|
|
878
|
-
|
|
1011
|
+
// EL TURNO ACABA SIN UNA SOLA LETRA QUE ENSENAR
|
|
1012
|
+
//
|
|
1013
|
+
// Salio de un caso real: cuatro ordenes seguidas ("puedes conectarte a mi
|
|
1014
|
+
// vscode?", "si?", "puedes?", "a mi vscode") contestadas todas con esto:
|
|
1015
|
+
//
|
|
1016
|
+
// 💭 penso 1s
|
|
1017
|
+
// ⚡ 4.8s · 0 tokens salida · $0.00000 USD · 100% contexto libre
|
|
1018
|
+
//
|
|
1019
|
+
// Ni un caracter en pantalla, ni un error, ni un aviso. Desde fuera es
|
|
1020
|
+
// identico a que el agente te ignore, y el usuario reescribe la orden
|
|
1021
|
+
// pensando que no llego. Encima cada una de esas cuatro se llevo por
|
|
1022
|
+
// delante un hueco de peticion en el proxy, asi que la quinta choco con
|
|
1023
|
+
// "Tienes 4 peticiones en curso": el silencio no solo confunde, ademas
|
|
1024
|
+
// deja al usuario sin servicio.
|
|
1025
|
+
//
|
|
1026
|
+
// Es el mismo agujero que el corte por longitud, en su otra cara: el
|
|
1027
|
+
// codigo no distingue "el modelo termino" de "no vino nada". Sin
|
|
1028
|
+
// herramientas y sin texto, el bucle daba el turno por hecho, imprimia el
|
|
1029
|
+
// pie y volvia al prompt.
|
|
1030
|
+
if (!content.trim()) {
|
|
1031
|
+
respuestasVacias++;
|
|
1032
|
+
// Se reintenta UNA vez, no mas, y aqui la moderacion importa: cada
|
|
1033
|
+
// intento consume un hueco de peticion de la licencia, y solo hay
|
|
1034
|
+
// cuatro. Insistir con un motor que no contesta es la forma mas rapida
|
|
1035
|
+
// de quedarse sin ninguno.
|
|
1036
|
+
if (respuestasVacias > MAX_RESPUESTAS_VACIAS) {
|
|
1037
|
+
const aviso = `El motor devolvio ${respuestasVacias} respuestas vacias seguidas. ` +
|
|
1038
|
+
"No es tu conexion ni tu orden: no esta llegando nada del modelo. " +
|
|
1039
|
+
"Espera un momento y reintenta, o /clear si venias de una conversacion larga.";
|
|
1040
|
+
salida.linea(chalk.yellow(`\n ⚠ ${aviso}\n`));
|
|
1041
|
+
return parada("error-motor", aviso);
|
|
1042
|
+
}
|
|
1043
|
+
salida.linea(chalk.gray("\n ⚠ El motor no devolvio nada. Reintentando una vez…\n"));
|
|
879
1044
|
this.messages.push({
|
|
880
|
-
role: "
|
|
881
|
-
content
|
|
1045
|
+
role: "user",
|
|
1046
|
+
content: "Tu respuesta anterior llego vacia: ni texto ni llamadas a herramienta. " +
|
|
1047
|
+
"El usuario no ha visto absolutamente nada. Contesta a lo que te pidio, " +
|
|
1048
|
+
"aunque sea para decir que no puedes hacerlo o que no lo has entendido.",
|
|
882
1049
|
});
|
|
1050
|
+
continue;
|
|
883
1051
|
}
|
|
1052
|
+
// Una respuesta con contenido cierra la racha: lo que cuenta son las
|
|
1053
|
+
// vacias SEGUIDAS, no las de toda la sesion.
|
|
1054
|
+
respuestasVacias = 0;
|
|
1055
|
+
// If text content was printed, ensure newline and record assistant message
|
|
1056
|
+
salida.crudo("\n\n");
|
|
1057
|
+
this.messages.push({
|
|
1058
|
+
role: "assistant",
|
|
1059
|
+
content,
|
|
1060
|
+
});
|
|
884
1061
|
// El modelo da el turno por cerrado. Si toco archivos y el proyecto tiene
|
|
885
1062
|
// verificacion automatica activada, se comprueba ANTES de creerselo: un
|
|
886
1063
|
// error de tipos que nadie mira se queda en el disco y los turnos
|
|
@@ -914,19 +1091,19 @@ export class AgentLoop {
|
|
|
914
1091
|
const tras = checkQuota();
|
|
915
1092
|
renderFooter(formattedCost, turnCompletionTokens, turnCachedTokens, this.currentEspectro.name, durationMs, contextPercent, quotaBadge(tras));
|
|
916
1093
|
if (auxiliar.total > 0) {
|
|
917
|
-
|
|
1094
|
+
salida.linea(chalk.gray(` + $${auxiliar.total.toFixed(4)} USD fuera del bucle (${auxiliar.rutas}), ya contados en la cuota.\n`));
|
|
918
1095
|
}
|
|
919
1096
|
// Aviso al acercarse, no solo al chocar: quedarse a medias sin previo aviso
|
|
920
1097
|
// es lo que hace que la gente cancele.
|
|
921
1098
|
if (tras.allowed && tras.spentWeekUsd >= tras.config.weeklyBudgetUsd * 0.8) {
|
|
922
1099
|
const queda = Math.max(0, tras.config.weeklyBudgetUsd - tras.spentWeekUsd);
|
|
923
|
-
|
|
1100
|
+
salida.linea(chalk.yellow(` ⚠ Te queda poca cuota semanal (${((queda / tras.config.weeklyBudgetUsd) * 100).toFixed(0)}%).\n`));
|
|
924
1101
|
}
|
|
925
1102
|
if (!completado) {
|
|
926
1103
|
// Se agotaron las iteraciones con trabajo a medias. En interactivo se ve
|
|
927
1104
|
// porque el ultimo mensaje queda cortado; en CI no lo ve nadie.
|
|
928
1105
|
const aviso = `El turno se agoto en el limite de ${this.maxIterations} iteraciones sin cerrar la tarea.`;
|
|
929
|
-
|
|
1106
|
+
salida.linea(chalk.yellow(` ⚠ ${aviso}\n`));
|
|
930
1107
|
return parada("limite-iteraciones", aviso);
|
|
931
1108
|
}
|
|
932
1109
|
return parada("completado");
|
|
@@ -948,7 +1125,7 @@ export class AgentLoop {
|
|
|
948
1125
|
const command = typeof fnArgs?.command === "string" ? fnArgs.command : "";
|
|
949
1126
|
if (command) {
|
|
950
1127
|
if (isDangerousCommand(command)) {
|
|
951
|
-
|
|
1128
|
+
salida.linea(chalk.gray(" (este comando es destructivo: se seguira preguntando cada vez, no se puede recordar)\n"));
|
|
952
1129
|
return;
|
|
953
1130
|
}
|
|
954
1131
|
const firma = commandSignature(command);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Por donde habla el bucle del agente.
|
|
3
|
+
*
|
|
4
|
+
* POR QUE
|
|
5
|
+
*
|
|
6
|
+
* El bucle escribia con `console.log` en diecisiete sitios. Mientras el unico
|
|
7
|
+
* sitio donde se ve al agente sea una terminal eso no molesta, pero ata el
|
|
8
|
+
* motor a su pantalla: para poner el agente en un panel de VSCode, en una web o
|
|
9
|
+
* en una app, hay que arrancarle esos diecisiete `console.log` uno a uno, y
|
|
10
|
+
* cada uno es una ocasion de romper algo.
|
|
11
|
+
*
|
|
12
|
+
* Con esto en medio, el bucle deja de saber donde acaba lo que dice. La
|
|
13
|
+
* terminal pasa a ser UNA implementacion, no LA implementacion.
|
|
14
|
+
*
|
|
15
|
+
* POR QUE RECIBE EL TEXTO YA COLOREADO
|
|
16
|
+
*
|
|
17
|
+
* Es a proposito, y es lo que hace que este cambio no pueda romper nada: lo que
|
|
18
|
+
* el bucle manda es exactamente la misma cadena que antes le pasaba a
|
|
19
|
+
* `console.log`, colores incluidos. La salida por terminal queda byte a byte
|
|
20
|
+
* igual que ayer, y las 538 pruebas siguen valiendo como red.
|
|
21
|
+
*
|
|
22
|
+
* Un panel que quiera pintarlo a su manera quita los codigos de color con
|
|
23
|
+
* `sinColores()` y se queda con el texto. Cuando haga falta distinguir un aviso
|
|
24
|
+
* de una nota, se anaden metodos con significado; hoy no hace falta y adivinarlo
|
|
25
|
+
* seria inventar trabajo.
|
|
26
|
+
*/
|
|
27
|
+
export interface SalidaDelAgente {
|
|
28
|
+
/** Un mensaje suelto, con su salto de linea al final. Equivale a console.log. */
|
|
29
|
+
linea(texto: string): void;
|
|
30
|
+
/** Escritura sin salto, para separar bloques. Equivale a process.stdout.write. */
|
|
31
|
+
crudo(texto: string): void;
|
|
32
|
+
}
|
|
33
|
+
/** Lo que usa el bucle. No sabe ni le importa quien esta al otro lado. */
|
|
34
|
+
export declare const salida: SalidaDelAgente;
|
|
35
|
+
/**
|
|
36
|
+
* Cambia a donde habla el agente. Con `null` vuelve a la terminal.
|
|
37
|
+
*
|
|
38
|
+
* Lo usara el panel de VSCode. Devolver siempre a la terminal al terminar: un
|
|
39
|
+
* agente que habla a un panel cerrado no habla con nadie.
|
|
40
|
+
*/
|
|
41
|
+
export declare function dirigirSalidaDelAgente(destino: SalidaDelAgente | null): void;
|
|
42
|
+
/** Quita los codigos de color, para quien pinte el texto a su manera. */
|
|
43
|
+
export declare function sinColores(texto: string): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { escribirEncimaDelPie } from "../ui/pieFijo.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pasa por `pieFijo` y no por `console.log` a secas.
|
|
4
|
+
*
|
|
5
|
+
* Mientras el spinner esta puesto hay un bloque pintado abajo del todo. Escribir
|
|
6
|
+
* directo lo pisaria: la respuesta saldria encima de la linea del spinner y el
|
|
7
|
+
* repintado siguiente ya no sabria donde empieza su bloque. Pasando por aqui, el
|
|
8
|
+
* pie se aparta, la linea entra en el historial como cualquier otra, y el pie
|
|
9
|
+
* vuelve a pintarse debajo.
|
|
10
|
+
*
|
|
11
|
+
* Sin pie puesto -que es casi todo el rato- esto es un `write` normal.
|
|
12
|
+
*/
|
|
13
|
+
const A_LA_TERMINAL = {
|
|
14
|
+
linea: (texto) => {
|
|
15
|
+
escribirEncimaDelPie(`${texto}\n`);
|
|
16
|
+
},
|
|
17
|
+
crudo: (texto) => {
|
|
18
|
+
escribirEncimaDelPie(texto);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
let actual = A_LA_TERMINAL;
|
|
22
|
+
/** Lo que usa el bucle. No sabe ni le importa quien esta al otro lado. */
|
|
23
|
+
export const salida = {
|
|
24
|
+
linea: (texto) => actual.linea(texto),
|
|
25
|
+
crudo: (texto) => actual.crudo(texto),
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Cambia a donde habla el agente. Con `null` vuelve a la terminal.
|
|
29
|
+
*
|
|
30
|
+
* Lo usara el panel de VSCode. Devolver siempre a la terminal al terminar: un
|
|
31
|
+
* agente que habla a un panel cerrado no habla con nadie.
|
|
32
|
+
*/
|
|
33
|
+
export function dirigirSalidaDelAgente(destino) {
|
|
34
|
+
actual = destino ?? A_LA_TERMINAL;
|
|
35
|
+
}
|
|
36
|
+
/** Quita los codigos de color, para quien pinte el texto a su manera. */
|
|
37
|
+
export function sinColores(texto) {
|
|
38
|
+
return String(texto).replace(/\x1b\[[0-9;]*m/g, "");
|
|
39
|
+
}
|
package/dist/agent/toolGate.js
CHANGED
|
@@ -3,7 +3,7 @@ import chalk from "chalk";
|
|
|
3
3
|
import { PermissionManager, commandSignature } from "../config/permissions.js";
|
|
4
4
|
import { HookManager } from "../hooks/manager.js";
|
|
5
5
|
import { askToolPermission } from "../ui/permissionPrompt.js";
|
|
6
|
-
import { releaseKeyboard } from "../ui/interrupt.js";
|
|
6
|
+
import { releaseKeyboard, retomarTeclado } from "../ui/interrupt.js";
|
|
7
7
|
let activa = null;
|
|
8
8
|
/** Lo llama el AgentLoop al construirse. */
|
|
9
9
|
export function registerToolGate(permissions, hooks) {
|
|
@@ -75,6 +75,11 @@ export async function autorizarHerramienta(toolName, args, cwd = process.cwd(),
|
|
|
75
75
|
releaseKeyboard();
|
|
76
76
|
console.log(chalk.gray(`\n${origen} pide permiso para usar "${toolName}":`));
|
|
77
77
|
const res = await askToolPermission(toolName, finales, cwd);
|
|
78
|
+
// Se devuelve el teclado a quien lo tenia. Sin esto, el resto del turno se
|
|
79
|
+
// queda sin vigilante: Esc deja de cancelar, lo que el usuario escriba no se
|
|
80
|
+
// encola, y el terminal vuelve al modo cocido -donde lo tecleado no aparece
|
|
81
|
+
// hasta pulsar Enter, que es como se veia el "se queda trabado".
|
|
82
|
+
retomarTeclado();
|
|
78
83
|
if (!res.approved) {
|
|
79
84
|
return {
|
|
80
85
|
permitida: false,
|
package/dist/config/engine.d.ts
CHANGED
|
@@ -38,7 +38,27 @@ export declare function engineCredential(fallbackApiKey: string): string;
|
|
|
38
38
|
* cualquiera apuntando al mismo endpoint.
|
|
39
39
|
*/
|
|
40
40
|
export declare function engineHeaders(route: EngineRoute): Record<string, string>;
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Cliente del motor ya configurado. Es el unico sitio donde se construye.
|
|
43
|
+
*
|
|
44
|
+
* POR QUE `maxRetries: 0`
|
|
45
|
+
*
|
|
46
|
+
* El SDK reintenta por su cuenta dos veces mas (su valor por defecto), y encima
|
|
47
|
+
* de eso el agente pasa cada llamada por `withRetry`. Dos capas apiladas, y solo
|
|
48
|
+
* una de las dos sabe algo del proxy.
|
|
49
|
+
*
|
|
50
|
+
* Medido contra un servidor de mentira: una sola llamada que recibe un 429 de
|
|
51
|
+
* "tienes 4 peticiones en curso" sale por la red TRES veces antes de que el
|
|
52
|
+
* error llegue siquiera a nuestro codigo. Y nuestro codigo lo tiene bien
|
|
53
|
+
* clasificado -`isRetryable` devuelve false para ese 429 justamente porque cada
|
|
54
|
+
* intento ocupa un hueco-, pero para entonces ya se han gastado los otros dos.
|
|
55
|
+
*
|
|
56
|
+
* Asi es como un solo corte de conexion se comia los cuatro huecos de la
|
|
57
|
+
* licencia y dejaba al usuario tres minutos sin servicio. La politica de
|
|
58
|
+
* reintentos vive en `withRetry` y en un solo sitio: ahi se sabe que un
|
|
59
|
+
* desbordamiento de contexto no puede empezar a caber, y que insistir con los
|
|
60
|
+
* huecos ocupados es la forma mas rapida de quedarse sin ninguno.
|
|
61
|
+
*/
|
|
42
62
|
export declare function createEngineClient(apiKey: string, route: EngineRoute): OpenAI;
|
|
43
63
|
/**
|
|
44
64
|
* De que tipo es el fallo, sin mirar el texto ya traducido.
|