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.
- package/dist/agent/huecos.d.ts +58 -0
- package/dist/agent/huecos.js +81 -0
- package/dist/agent/loop.js +144 -24
- package/dist/agent/salidaDelAgente.js +10 -24
- 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/quota.d.ts +11 -0
- package/dist/config/quota.js +18 -0
- package/dist/index.js +35 -26
- package/dist/prompts/systemPrompt.js +186 -150
- package/dist/tools/browserExtension.js +118 -3
- package/dist/tools/computerUse.d.ts +1 -1
- package/dist/tools/computerUse.js +183 -2
- package/dist/tools/toolDefsComputer.js +6 -2
- package/dist/tools/visionBridge.js +60 -3
- package/dist/tools/win/hostScript.js +154 -0
- package/dist/ui/banner.d.ts +31 -0
- package/dist/ui/banner.js +45 -0
- package/dist/ui/comandos.d.ts +33 -0
- package/dist/ui/comandos.js +49 -0
- package/dist/ui/informeDeUso.d.ts +17 -0
- package/dist/ui/informeDeUso.js +81 -0
- package/dist/ui/interrupt.d.ts +10 -0
- package/dist/ui/interrupt.js +50 -2
- package/dist/ui/marco.d.ts +35 -0
- package/dist/ui/marco.js +161 -0
- package/dist/ui/pieFijo.d.ts +56 -0
- package/dist/ui/pieFijo.js +309 -0
- package/dist/ui/prompt.d.ts +4 -41
- package/dist/ui/prompt.js +24 -157
- package/dist/ui/reasoningStream.js +22 -12
- package/dist/ui/renderer.d.ts +13 -0
- package/dist/ui/renderer.js +41 -12
- package/dist/ui/spinner.d.ts +43 -8
- package/dist/ui/spinner.js +118 -43
- package/extension/background.js +9 -3
- 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/manifest.json +60 -60
- 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
|
+
}
|
package/dist/agent/loop.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
954
|
-
"No es
|
|
955
|
-
"Espera un momento y reintenta, o
|
|
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
|
|
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
|
-
*
|
|
3
|
+
* Pasa por `pieFijo` y no por `console.log` a secas.
|
|
3
4
|
*
|
|
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.
|
|
5
10
|
*
|
|
6
|
-
*
|
|
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
|
-
|
|
15
|
+
escribirEncimaDelPie(`${texto}\n`);
|
|
30
16
|
},
|
|
31
17
|
crudo: (texto) => {
|
|
32
|
-
|
|
18
|
+
escribirEncimaDelPie(texto);
|
|
33
19
|
},
|
|
34
20
|
};
|
|
35
21
|
let actual = A_LA_TERMINAL;
|
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.
|
package/dist/config/engine.js
CHANGED
|
@@ -55,12 +55,33 @@ export function engineHeaders(route) {
|
|
|
55
55
|
cabeceras["X-Chocolatito-Instance"] = license.instanceId;
|
|
56
56
|
return cabeceras;
|
|
57
57
|
}
|
|
58
|
-
/**
|
|
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) {
|
package/dist/config/quota.d.ts
CHANGED
|
@@ -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. */
|
package/dist/config/quota.js
CHANGED
|
@@ -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;
|