chocolatito-code 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/agent/huecos.d.ts +58 -0
  2. package/dist/agent/huecos.js +81 -0
  3. package/dist/agent/loop.js +144 -24
  4. package/dist/agent/salidaDelAgente.js +10 -24
  5. package/dist/agent/toolGate.js +6 -1
  6. package/dist/config/engine.d.ts +21 -1
  7. package/dist/config/engine.js +22 -1
  8. package/dist/config/quota.d.ts +11 -0
  9. package/dist/config/quota.js +18 -0
  10. package/dist/index.js +35 -26
  11. package/dist/prompts/systemPrompt.js +186 -150
  12. package/dist/tools/browserExtension.js +118 -3
  13. package/dist/tools/computerUse.d.ts +1 -1
  14. package/dist/tools/computerUse.js +183 -2
  15. package/dist/tools/toolDefsComputer.js +6 -2
  16. package/dist/tools/visionBridge.js +60 -3
  17. package/dist/tools/win/hostScript.js +154 -0
  18. package/dist/ui/banner.d.ts +31 -0
  19. package/dist/ui/banner.js +45 -0
  20. package/dist/ui/comandos.d.ts +33 -0
  21. package/dist/ui/comandos.js +49 -0
  22. package/dist/ui/informeDeUso.d.ts +17 -0
  23. package/dist/ui/informeDeUso.js +81 -0
  24. package/dist/ui/interrupt.d.ts +10 -0
  25. package/dist/ui/interrupt.js +50 -2
  26. package/dist/ui/marco.d.ts +35 -0
  27. package/dist/ui/marco.js +161 -0
  28. package/dist/ui/pieFijo.d.ts +56 -0
  29. package/dist/ui/pieFijo.js +309 -0
  30. package/dist/ui/prompt.d.ts +4 -41
  31. package/dist/ui/prompt.js +24 -157
  32. package/dist/ui/reasoningStream.js +22 -12
  33. package/dist/ui/renderer.d.ts +13 -0
  34. package/dist/ui/renderer.js +41 -12
  35. package/dist/ui/spinner.d.ts +43 -8
  36. package/dist/ui/spinner.js +118 -43
  37. package/extension/background.js +9 -3
  38. package/extension/iconos/128.png +0 -0
  39. package/extension/iconos/16.png +0 -0
  40. package/extension/iconos/32.png +0 -0
  41. package/extension/iconos/48.png +0 -0
  42. package/extension/iconos/ORIGEN.md +48 -0
  43. package/extension/manifest.json +60 -60
  44. package/package.json +67 -67
@@ -0,0 +1,58 @@
1
+ /**
2
+ * NO GASTAR UN HUECO DE PETICION QUE YA SABEMOS QUE NO HAY
3
+ *
4
+ * DE DONDE SALE
5
+ *
6
+ * Sesion real. Una respuesta se corto a mitad, el bucle reintento, y en cuatro
7
+ * mensajes el usuario se quedo sin servicio:
8
+ *
9
+ * ⏸ Tienes 4 peticiones en curso. Reintenta en 197s.
10
+ * ❯ q paso?
11
+ * ⏸ Tienes 4 peticiones en curso. Reintenta en 189s.
12
+ *
13
+ * Fijate en la segunda. El agente ya SABIA que no habia huecos y que faltaban
14
+ * 197 segundos; ocho segundos despues volvio a llamar al motor para que se lo
15
+ * repitiera. Esa llamada no podia salir bien por definicion, y ademas es la
16
+ * clase de peticion que puede refrescar la reserva del proxy y alargar la
17
+ * espera. El usuario tecleaba, esperaba, y se llevaba la misma pared.
18
+ *
19
+ * LO QUE HACE
20
+ *
21
+ * Se apunta hasta cuando no hay huecos y se responde desde aqui, sin red. Deja
22
+ * de gastarse una peticion por cada intento y el usuario ve la cuenta atras
23
+ * bajar de verdad en lugar de quedarse clavada.
24
+ *
25
+ * POR QUE NO SE FIA DEL RELOJ A CIEGAS
26
+ *
27
+ * Un reloj que se adelanta desbloquea antes de tiempo -inofensivo, la siguiente
28
+ * peticion dira la verdad-. Pero uno que se atrasa dejaria `ocupadoHasta` en un
29
+ * futuro lejano y el agente se negaria a llamar al motor durante horas sin que
30
+ * nada lo despierte. Por eso la espera nunca puede pasar de la ventana que se
31
+ * anoto: en el peor caso se espera de mas una vez, no para siempre.
32
+ */
33
+ /** Cuando el proxy no dice cuanto falta. Un minuto es su reserva mas corta. */
34
+ export declare const SEGUNDOS_POR_DEFECTO = 60;
35
+ /** Techo de la espera. Ninguna reserva del proxy dura mas de unos minutos. */
36
+ export declare const SEGUNDOS_MAXIMOS = 300;
37
+ /**
38
+ * Apunta que los huecos estan llenos.
39
+ *
40
+ * `retryAfterS` es lo que dice el proxy en `error.retry_after_s`. Si no viene,
41
+ * o viene con un valor absurdo, se usa un minuto: adivinar de mas encierra al
42
+ * usuario y adivinar de menos le devuelve a la misma pared.
43
+ */
44
+ export declare function anotarHuecosOcupados(retryAfterS?: unknown): void;
45
+ /** Segundos que faltan para volver a tener hueco. 0 si ya lo hay. */
46
+ export declare function segundosDeEspera(): number;
47
+ /**
48
+ * Olvida la espera. Se llama en cuanto el motor concede una peticion: si
49
+ * contesta, es que habia hueco, y seguir bloqueando seria mentir.
50
+ */
51
+ export declare function liberarHuecos(): void;
52
+ /**
53
+ * Lo que se le dice al usuario en vez de llamar al motor.
54
+ *
55
+ * Dice de donde sale la cuenta -no es una invencion del cliente- y que no tiene
56
+ * que hacer nada: es lo unico que se arregla solo con esperar.
57
+ */
58
+ export declare function mensajeDeEspera(segundos: number): string;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * NO GASTAR UN HUECO DE PETICION QUE YA SABEMOS QUE NO HAY
3
+ *
4
+ * DE DONDE SALE
5
+ *
6
+ * Sesion real. Una respuesta se corto a mitad, el bucle reintento, y en cuatro
7
+ * mensajes el usuario se quedo sin servicio:
8
+ *
9
+ * ⏸ Tienes 4 peticiones en curso. Reintenta en 197s.
10
+ * ❯ q paso?
11
+ * ⏸ Tienes 4 peticiones en curso. Reintenta en 189s.
12
+ *
13
+ * Fijate en la segunda. El agente ya SABIA que no habia huecos y que faltaban
14
+ * 197 segundos; ocho segundos despues volvio a llamar al motor para que se lo
15
+ * repitiera. Esa llamada no podia salir bien por definicion, y ademas es la
16
+ * clase de peticion que puede refrescar la reserva del proxy y alargar la
17
+ * espera. El usuario tecleaba, esperaba, y se llevaba la misma pared.
18
+ *
19
+ * LO QUE HACE
20
+ *
21
+ * Se apunta hasta cuando no hay huecos y se responde desde aqui, sin red. Deja
22
+ * de gastarse una peticion por cada intento y el usuario ve la cuenta atras
23
+ * bajar de verdad en lugar de quedarse clavada.
24
+ *
25
+ * POR QUE NO SE FIA DEL RELOJ A CIEGAS
26
+ *
27
+ * Un reloj que se adelanta desbloquea antes de tiempo -inofensivo, la siguiente
28
+ * peticion dira la verdad-. Pero uno que se atrasa dejaria `ocupadoHasta` en un
29
+ * futuro lejano y el agente se negaria a llamar al motor durante horas sin que
30
+ * nada lo despierte. Por eso la espera nunca puede pasar de la ventana que se
31
+ * anoto: en el peor caso se espera de mas una vez, no para siempre.
32
+ */
33
+ /** Cuando el proxy no dice cuanto falta. Un minuto es su reserva mas corta. */
34
+ export const SEGUNDOS_POR_DEFECTO = 60;
35
+ /** Techo de la espera. Ninguna reserva del proxy dura mas de unos minutos. */
36
+ export const SEGUNDOS_MAXIMOS = 300;
37
+ let ocupadoHasta = 0;
38
+ let ventanaMs = 0;
39
+ /**
40
+ * Apunta que los huecos estan llenos.
41
+ *
42
+ * `retryAfterS` es lo que dice el proxy en `error.retry_after_s`. Si no viene,
43
+ * o viene con un valor absurdo, se usa un minuto: adivinar de mas encierra al
44
+ * usuario y adivinar de menos le devuelve a la misma pared.
45
+ */
46
+ export function anotarHuecosOcupados(retryAfterS) {
47
+ const dicho = Number(retryAfterS);
48
+ const segundos = Number.isFinite(dicho) && dicho > 0 ? Math.min(dicho, SEGUNDOS_MAXIMOS) : SEGUNDOS_POR_DEFECTO;
49
+ ventanaMs = segundos * 1000;
50
+ ocupadoHasta = Date.now() + ventanaMs;
51
+ }
52
+ /** Segundos que faltan para volver a tener hueco. 0 si ya lo hay. */
53
+ export function segundosDeEspera() {
54
+ if (ocupadoHasta === 0)
55
+ return 0;
56
+ const faltan = Math.min(ocupadoHasta - Date.now(), ventanaMs);
57
+ if (faltan <= 0) {
58
+ liberarHuecos();
59
+ return 0;
60
+ }
61
+ return Math.ceil(faltan / 1000);
62
+ }
63
+ /**
64
+ * Olvida la espera. Se llama en cuanto el motor concede una peticion: si
65
+ * contesta, es que habia hueco, y seguir bloqueando seria mentir.
66
+ */
67
+ export function liberarHuecos() {
68
+ ocupadoHasta = 0;
69
+ ventanaMs = 0;
70
+ }
71
+ /**
72
+ * Lo que se le dice al usuario en vez de llamar al motor.
73
+ *
74
+ * Dice de donde sale la cuenta -no es una invencion del cliente- y que no tiene
75
+ * que hacer nada: es lo unico que se arregla solo con esperar.
76
+ */
77
+ export function mensajeDeEspera(segundos) {
78
+ return (`Los huecos de peticion de tu licencia siguen ocupados. Faltan ${segundos}s. ` +
79
+ "No llamo al motor porque la respuesta seria la misma y cada intento puede alargar la espera. " +
80
+ "No hace falta que hagas nada: el hueco se libera solo.");
81
+ }
@@ -3,17 +3,18 @@ import process from "node:process";
3
3
  import { getAllTools } from "../tools/definitions.js";
4
4
  import { executeToolCall } from "../tools/runner.js";
5
5
  import { TokenTracker } from "./tracker.js";
6
- import { renderToolStart, renderToolSuccess, renderToolError, renderDiff, renderFooter, renderError, } from "../ui/renderer.js";
6
+ import { renderToolStart, renderToolSuccess, renderToolError, renderDiff, renderFooter, renderError, formatToolName, } 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";
14
15
  import { PermissionManager, commandSignature, isDangerousCommand } from "../config/permissions.js";
15
16
  import { askToolPermission } from "../ui/permissionPrompt.js";
16
- import { DynamicSpinner } from "../ui/spinner.js";
17
+ import { DynamicSpinner, conSpinner } from "../ui/spinner.js";
17
18
  import { globalUndoManager } from "./undoManager.js";
18
19
  import { salida } from "./salidaDelAgente.js";
19
20
  import { InterruptWatcher, isAbortError } from "../ui/interrupt.js";
@@ -76,8 +77,8 @@ function parsearArgumentos(bruto) {
76
77
  /** Lo que se le dice al modelo cuando sus argumentos llegaron a medias. */
77
78
  function mensajeDeArgumentosRotos(herramienta) {
78
79
  return (`No se ejecuto "${herramienta}": los argumentos llegaron incompletos, seguramente porque la ` +
79
- `respuesta se corto por longitud. No se ha hecho nada.
80
-
80
+ `respuesta se corto por longitud. No se ha hecho nada.
81
+
81
82
  ` +
82
83
  `Si era un archivo largo, hazlo por partes: una primera llamada a write_file con el principio ` +
83
84
  `y las siguientes con modo="anadir" para ir pegando el resto. Cada trozo, por debajo de unas ` +
@@ -455,6 +456,15 @@ export class AgentLoop {
455
456
  tokensSalida: turnCompletionTokens,
456
457
  };
457
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
+ }
458
468
  // Cuota ANTES de gastar. Si no queda margen, se dice y no se llama al motor:
459
469
  // avisar despues de haber gastado no sirve de nada.
460
470
  const cuota = checkQuota();
@@ -553,10 +563,15 @@ export class AgentLoop {
553
563
  // Un 402 (cuota agotada) o un 401 (licencia caida) no son "fallos del
554
564
  // motor" y ademas no se reintentan nunca: decirlo como tal confunde y
555
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);
556
571
  const claro = explicarErrorDelMotor(err);
557
572
  if (claro) {
558
- salida.linea(chalk.yellow(`
559
- ⏸ ${claro}
573
+ salida.linea(chalk.yellow(`
574
+ ⏸ ${claro}
560
575
  `));
561
576
  return parada(motivoDeParadaDelError(err), claro);
562
577
  }
@@ -564,19 +579,34 @@ export class AgentLoop {
564
579
  renderError(fallo);
565
580
  return parada("error-motor", fallo);
566
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();
567
585
  let content = "";
568
586
  let reasoning = "";
569
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;
570
599
  const rawToolCalls = [];
571
- let isFirstChunk = true;
572
600
  const reasoningStream = new ReasoningStream();
573
- const markdown = new MarkdownStream();
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));
574
604
  try {
575
605
  for await (const chunk of stream) {
576
- if (isFirstChunk) {
577
- spinner.stop();
578
- isFirstChunk = false;
579
- }
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.
580
610
  const delta = chunk.choices?.[0]?.delta;
581
611
  // Handle real-time reasoning stream
582
612
  if (delta?.reasoning_content) {
@@ -590,6 +620,12 @@ export class AgentLoop {
590
620
  content += delta.content;
591
621
  markdown.push(delta.content);
592
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);
593
629
  // Accumulate fragmented tool calls by index tc.index
594
630
  if (delta?.tool_calls) {
595
631
  for (const tc of delta.tool_calls) {
@@ -612,10 +648,13 @@ export class AgentLoop {
612
648
  // El motivo por el que el modelo dejo de hablar. Sin esto, quedarse
613
649
  // sin presupuesto de salida es indistinguible de terminar el turno.
614
650
  const corte = chunk.choices?.[0]?.finish_reason;
615
- if (corte)
651
+ if (corte) {
616
652
  motivoDeCorte = corte;
653
+ llegoElFinal = true;
654
+ }
617
655
  // Track streaming usage metrics
618
656
  if (chunk.usage) {
657
+ llegoElFinal = true;
619
658
  this.tracker.addUsage(chunk.usage);
620
659
  turnPromptTokens += chunk.usage.prompt_tokens || 0;
621
660
  if (chunk.usage.prompt_tokens)
@@ -639,10 +678,12 @@ export class AgentLoop {
639
678
  salida.linea(chalk.yellow("\n⏸ Interrumpido. Dime como seguir."));
640
679
  return parada("interrumpido");
641
680
  }
681
+ if (esDemasiadasEnCurso(streamErr))
682
+ anotarHuecosOcupados(streamErr?.error?.retry_after_s);
642
683
  const claroStream = explicarErrorDelMotor(streamErr);
643
684
  if (claroStream) {
644
- salida.linea(chalk.yellow(`
645
- ⏸ ${claroStream}
685
+ salida.linea(chalk.yellow(`
686
+ ⏸ ${claroStream}
646
687
  `));
647
688
  return parada(motivoDeParadaDelError(streamErr), claroStream);
648
689
  }
@@ -662,6 +703,71 @@ export class AgentLoop {
662
703
  }
663
704
  // Filter valid accumulated tool calls
664
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
+ //
731
+ // NO SE EXIGE QUE HAYA LLEGADO TEXTO. La primera version pedia contenido o
732
+ // herramientas, y se le escapaba el caso mas comun: el corte que pasa
733
+ // DESPUES del razonamiento y ANTES de la primera palabra. Eso caia en la
734
+ // rama de "respuesta vacia", que si reintenta, y volvia a gastar huecos:
735
+ //
736
+ // 💭 penso 2s
737
+ // ⚠ El motor no devolvio nada. Reintentando una vez…
738
+ //
739
+ // Sin marca de final, el stream se corto. Da igual cuanto habia llegado.
740
+ // Una respuesta vacia DE VERDAD trae su finish_reason: el motor cerro el
741
+ // turno en regla y simplemente no dijo nada, y eso si merece un reintento
742
+ // porque la conexion estaba bien.
743
+ if (!llegoElFinal) {
744
+ watcher.stop();
745
+ // Lo que llego se guarda, pero MARCADO. Sin la marca, en el turno
746
+ // siguiente el modelo lee su propia frase a medias como algo que decidio
747
+ // dejar asi, y responde a un texto que nadie escribio.
748
+ if (content.trim()) {
749
+ this.messages.push({
750
+ role: "assistant",
751
+ content: `${content}\n\n[Aqui se corto: la conexion con el motor se cerro a mitad de la respuesta.]`,
752
+ });
753
+ }
754
+ // Se dice HASTA DONDE llego, y no por cortesia: es el unico dato que
755
+ // distingue un corte antes de empezar de uno a mitad de frase, y en un
756
+ // pegado del usuario es lo unico con lo que se puede diagnosticar.
757
+ // Las llamadas a herramienta de un stream cortado NO se ejecutan: sus
758
+ // argumentos estan tan a medias como el texto.
759
+ const hastaDonde = content.trim()
760
+ ? "Lo que llego se queda arriba."
761
+ : reasoning.trim()
762
+ ? "Se corto antes de escribir nada: solo llego el razonamiento."
763
+ : "Se corto antes de que llegara nada.";
764
+ const aviso = "La respuesta se corto: la conexion con el motor se cerro sin decir por que (no llego " +
765
+ `ni el motivo de fin ni el recuento del gasto). ${hastaDonde} ` +
766
+ "No lo reintento solo porque cada intento ocupa uno de los cuatro huecos de tu licencia " +
767
+ "durante varios minutos; vuelve a pedirmelo y sigo.";
768
+ salida.linea(chalk.yellow(`\n ⚠ ${aviso}\n`));
769
+ return parada("error-motor", aviso);
770
+ }
665
771
  // El modelo se quedo sin presupuesto de salida y no llego a pedir nada.
666
772
  //
667
773
  // Esto se veia EXACTAMENTE igual que un turno terminado: sin llamadas a
@@ -761,10 +867,14 @@ export class AgentLoop {
761
867
  for (const { tc, args } of autorizadas) {
762
868
  renderToolStart(tc.function.name, getToolSummary(tc.function.name, args));
763
869
  }
764
- const settled = await Promise.all(autorizadas.map(async ({ tc, args, pos }) => {
870
+ // Un solo spinner para el lote entero: uno por herramienta se pisarian
871
+ // entre ellos y el pie parpadearia tantas veces como llamadas haya.
872
+ const settled = await conSpinner(autorizadas.length === 1
873
+ ? formatToolName(autorizadas[0].tc.function.name)
874
+ : `${autorizadas.length} herramientas a la vez`, () => Promise.all(autorizadas.map(async ({ tc, args, pos }) => {
765
875
  const r = await executeToolCall(tc.function.name, args, this.currentCwd, this.apiKey, this.currentEspectro.underlyingModel);
766
876
  return { tc, args, pos, result: r.result };
767
- }));
877
+ })));
768
878
  for (const { tc, args, pos, result } of settled) {
769
879
  renderToolSuccess(tc.function.name, result);
770
880
  const aviso = await notificarPostHerramienta(tc.function.name, args, result, this.currentCwd);
@@ -885,7 +995,10 @@ export class AgentLoop {
885
995
  }
886
996
  const summary = getToolSummary(fnName, fnArgs);
887
997
  renderToolStart(fnName, summary);
888
- const { result, diffText, newCwd } = await executeToolCall(fnName, fnArgs, this.currentCwd, this.apiKey, this.currentEspectro.underlyingModel);
998
+ // El pie se queda puesto mientras dure la herramienta. Un run_command
999
+ // de veinte segundos dejaba la pantalla muda: ni cronometro, ni forma
1000
+ // de saber que seguia vivo, ni donde escribir para encolar algo.
1001
+ const { result, diffText, newCwd } = await conSpinner(formatToolName(fnName), () => executeToolCall(fnName, fnArgs, this.currentCwd, this.apiKey, this.currentEspectro.underlyingModel));
889
1002
  if (newCwd) {
890
1003
  this.currentCwd = newCwd;
891
1004
  }
@@ -910,7 +1023,7 @@ export class AgentLoop {
910
1023
  toolResults.push({
911
1024
  role: "tool",
912
1025
  tool_call_id: tc.id,
913
- content: post.additionalContext ? `${texto}\n\n
1026
+ content: post.additionalContext ? `${texto}\n\n
914
1027
  [Hook PostToolUse]: ${post.additionalContext}` : texto,
915
1028
  });
916
1029
  }
@@ -943,6 +1056,12 @@ export class AgentLoop {
943
1056
  // codigo no distingue "el modelo termino" de "no vino nada". Sin
944
1057
  // herramientas y sin texto, el bucle daba el turno por hecho, imprimia el
945
1058
  // pie y volvia al prompt.
1059
+ //
1060
+ // Aqui solo se llega con la marca de final puesta: un stream cortado sale
1061
+ // mucho antes, por su propia rama. Asi que esto es lo que parece -el motor
1062
+ // cerro el turno en regla y no dijo nada- y no un corte disfrazado. Esa
1063
+ // separacion es la que hace que reintentar aqui sea razonable: la conexion
1064
+ // estaba bien, y por eso el reintento no es tirar un hueco a la basura.
946
1065
  if (!content.trim()) {
947
1066
  respuestasVacias++;
948
1067
  // Se reintenta UNA vez, no mas, y aqui la moderacion importa: cada
@@ -950,13 +1069,14 @@ export class AgentLoop {
950
1069
  // cuatro. Insistir con un motor que no contesta es la forma mas rapida
951
1070
  // de quedarse sin ninguno.
952
1071
  if (respuestasVacias > MAX_RESPUESTAS_VACIAS) {
953
- const aviso = `El motor devolvio ${respuestasVacias} respuestas vacias seguidas. ` +
954
- "No es tu conexion ni tu orden: no esta llegando nada del modelo. " +
955
- "Espera un momento y reintenta, o /clear si venias de una conversacion larga.";
1072
+ const aviso = `El motor cerro el turno en regla ${respuestasVacias} veces seguidas sin una sola ` +
1073
+ "palabra. No es un corte de conexion -eso se avisa aparte y con otras palabras-: la " +
1074
+ "peticion llego y volvio entera, solo que vacia. Espera un momento y reintenta, o " +
1075
+ "/clear si venias de una conversacion larga.";
956
1076
  salida.linea(chalk.yellow(`\n ⚠ ${aviso}\n`));
957
1077
  return parada("error-motor", aviso);
958
1078
  }
959
- salida.linea(chalk.gray("\n ⚠ El motor no devolvio nada. Reintentando una vez…\n"));
1079
+ salida.linea(chalk.gray("\n ⚠ El motor cerro el turno sin decir nada. Reintentando una vez…\n"));
960
1080
  this.messages.push({
961
1081
  role: "user",
962
1082
  content: "Tu respuesta anterior llego vacia: ni texto ni llamadas a herramienta. " +
@@ -1,35 +1,21 @@
1
+ import { escribirEncimaDelPie } from "../ui/pieFijo.js";
1
2
  /**
2
- * Por donde habla el bucle del agente.
3
+ * Pasa por `pieFijo` y no por `console.log` a secas.
3
4
  *
4
- * POR QUE
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.
5
10
  *
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.
11
+ * Sin pie puesto -que es casi todo el rato- esto es un `write` normal.
26
12
  */
27
13
  const A_LA_TERMINAL = {
28
14
  linea: (texto) => {
29
- console.log(texto);
15
+ escribirEncimaDelPie(`${texto}\n`);
30
16
  },
31
17
  crudo: (texto) => {
32
- process.stdout.write(texto);
18
+ escribirEncimaDelPie(texto);
33
19
  },
34
20
  };
35
21
  let actual = A_LA_TERMINAL;
@@ -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,
@@ -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
- /** Cliente del motor ya configurado. Es el unico sitio donde se construye. */
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.
@@ -55,12 +55,33 @@ export function engineHeaders(route) {
55
55
  cabeceras["X-Chocolatito-Instance"] = license.instanceId;
56
56
  return cabeceras;
57
57
  }
58
- /** Cliente del motor ya configurado. Es el unico sitio donde se construye. */
58
+ /**
59
+ * Cliente del motor ya configurado. Es el unico sitio donde se construye.
60
+ *
61
+ * POR QUE `maxRetries: 0`
62
+ *
63
+ * El SDK reintenta por su cuenta dos veces mas (su valor por defecto), y encima
64
+ * de eso el agente pasa cada llamada por `withRetry`. Dos capas apiladas, y solo
65
+ * una de las dos sabe algo del proxy.
66
+ *
67
+ * Medido contra un servidor de mentira: una sola llamada que recibe un 429 de
68
+ * "tienes 4 peticiones en curso" sale por la red TRES veces antes de que el
69
+ * error llegue siquiera a nuestro codigo. Y nuestro codigo lo tiene bien
70
+ * clasificado -`isRetryable` devuelve false para ese 429 justamente porque cada
71
+ * intento ocupa un hueco-, pero para entonces ya se han gastado los otros dos.
72
+ *
73
+ * Asi es como un solo corte de conexion se comia los cuatro huecos de la
74
+ * licencia y dejaba al usuario tres minutos sin servicio. La politica de
75
+ * reintentos vive en `withRetry` y en un solo sitio: ahi se sabe que un
76
+ * desbordamiento de contexto no puede empezar a caber, y que insistir con los
77
+ * huecos ocupados es la forma mas rapida de quedarse sin ninguno.
78
+ */
59
79
  export function createEngineClient(apiKey, route) {
60
80
  return new OpenAI({
61
81
  apiKey: engineCredential(apiKey),
62
82
  baseURL: getEngineBaseUrl(),
63
83
  defaultHeaders: engineHeaders(route),
84
+ maxRetries: 0,
64
85
  });
65
86
  }
66
87
  export function clasificarErrorDelMotor(err) {
@@ -50,6 +50,17 @@ export interface QuotaStatus {
50
50
  config: QuotaConfig;
51
51
  }
52
52
  export declare function checkQuota(ahora?: number): QuotaStatus;
53
+ /**
54
+ * Cuando la ventana corta se vacia DEL TODO, si no se gasta nada mas.
55
+ *
56
+ * `seLiberaEn` dice cuando caduca el gasto mas viejo, que es lo que hace falta
57
+ * para saber cuando se puede volver a trabajar tras un corte. Para ensenar el
58
+ * consumo es otra pregunta: cuando vuelve a cero. Y como la ventana es
59
+ * deslizante, eso es cuando caduca el gasto mas RECIENTE.
60
+ *
61
+ * `null` si no hay nada dentro de la ventana: no hay nada que renovar.
62
+ */
63
+ export declare function ventanaVaciaEn(ahora?: number): Date | null;
53
64
  /** Cuanto falta para la hora indicada, en lenguaje llano. */
54
65
  export declare function faltaPara(fecha: Date, ahora?: number): string;
55
66
  /** Mensaje completo para cuando se agota la cuota. */
@@ -148,6 +148,24 @@ export function checkQuota(ahora = Date.now()) {
148
148
  }
149
149
  return { allowed: true, spentWindowUsd, spentWeekUsd, config };
150
150
  }
151
+ /**
152
+ * Cuando la ventana corta se vacia DEL TODO, si no se gasta nada mas.
153
+ *
154
+ * `seLiberaEn` dice cuando caduca el gasto mas viejo, que es lo que hace falta
155
+ * para saber cuando se puede volver a trabajar tras un corte. Para ensenar el
156
+ * consumo es otra pregunta: cuando vuelve a cero. Y como la ventana es
157
+ * deslizante, eso es cuando caduca el gasto mas RECIENTE.
158
+ *
159
+ * `null` si no hay nada dentro de la ventana: no hay nada que renovar.
160
+ */
161
+ export function ventanaVaciaEn(ahora = Date.now()) {
162
+ const ventanaMs = getQuotaConfig().windowHours * 60 * 60 * 1000;
163
+ const dentro = load().filter((e) => e.t >= ahora - ventanaMs);
164
+ if (dentro.length === 0)
165
+ return null;
166
+ const masReciente = dentro.reduce((max, e) => (e.t > max ? e.t : max), dentro[0].t);
167
+ return new Date(masReciente + ventanaMs);
168
+ }
151
169
  /** Cuanto falta para la hora indicada, en lenguaje llano. */
152
170
  export function faltaPara(fecha, ahora = Date.now()) {
153
171
  const ms = fecha.getTime() - ahora;