chocolatito-code 1.2.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/dist/agent/huecos.d.ts +58 -0
- package/dist/agent/huecos.js +81 -0
- package/dist/agent/loop.js +99 -15
- 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/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/interrupt.d.ts +10 -0
- package/dist/ui/interrupt.js +50 -2
- package/dist/ui/pieFijo.d.ts +56 -0
- package/dist/ui/pieFijo.js +309 -0
- package/dist/ui/prompt.js +9 -1
- package/dist/ui/reasoningStream.js +22 -12
- package/dist/ui/spinner.d.ts +15 -8
- package/dist/ui/spinner.js +80 -42
- 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 +1 -1
- package/package.json +1 -1
|
@@ -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
|
@@ -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";
|
|
@@ -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,49 @@ 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
|
+
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
|
+
}
|
|
665
749
|
// El modelo se quedo sin presupuesto de salida y no llego a pedir nada.
|
|
666
750
|
//
|
|
667
751
|
// Esto se veia EXACTAMENTE igual que un turno terminado: sin llamadas a
|
|
@@ -910,7 +994,7 @@ export class AgentLoop {
|
|
|
910
994
|
toolResults.push({
|
|
911
995
|
role: "tool",
|
|
912
996
|
tool_call_id: tc.id,
|
|
913
|
-
content: post.additionalContext ? `${texto}\n\n
|
|
997
|
+
content: post.additionalContext ? `${texto}\n\n
|
|
914
998
|
[Hook PostToolUse]: ${post.additionalContext}` : texto,
|
|
915
999
|
});
|
|
916
1000
|
}
|
|
@@ -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) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { WebSocketServer, WebSocket } from "ws";
|
|
5
6
|
import { describeScreen } from "./visionBridge.js";
|
|
6
7
|
/**
|
|
@@ -278,6 +279,55 @@ function emparejar(objetivo, origen, destino) {
|
|
|
278
279
|
return candidatos[pos].ref;
|
|
279
280
|
return null;
|
|
280
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Avisar cuando la extension de Chrome se quedo atras.
|
|
284
|
+
*
|
|
285
|
+
* POR QUE HACE FALTA
|
|
286
|
+
*
|
|
287
|
+
* El CLI se actualiza solo, y la extension viaja DENTRO del paquete npm. Asi
|
|
288
|
+
* que al actualizarse, los archivos de la extension en el disco pasan a ser
|
|
289
|
+
* nuevos... pero Chrome sigue ejecutando los que cargo en su dia. No se entera
|
|
290
|
+
* hasta que alguien pulsa "recargar" en chrome://extensions.
|
|
291
|
+
*
|
|
292
|
+
* Eso deja al usuario con una version del CLI que espera cosas que su extension
|
|
293
|
+
* no sabe hacer, y los fallos que salen de ahi no se parecen en nada a su causa:
|
|
294
|
+
* una captura que llega en PNG cuando el CLI ya cuenta con JPEG, una accion que
|
|
295
|
+
* contesta distinto de lo que se espera. Sintomas raros, causa invisible.
|
|
296
|
+
*
|
|
297
|
+
* La extension ya decia su version en cada `status` y nadie la miraba. Ahora se
|
|
298
|
+
* compara con la que trae este CLI, y si no cuadran se dice una vez.
|
|
299
|
+
*/
|
|
300
|
+
let yaAvisadoDeVersion = false;
|
|
301
|
+
/** La version de extension que trae ESTE CLI, leida del paquete instalado. */
|
|
302
|
+
function versionEsperadaDeExtension() {
|
|
303
|
+
try {
|
|
304
|
+
const aqui = path.dirname(fileURLToPath(import.meta.url));
|
|
305
|
+
const manifiesto = path.resolve(aqui, "..", "..", "extension", "manifest.json");
|
|
306
|
+
return String(JSON.parse(fs.readFileSync(manifiesto, "utf-8")).version || "");
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// Sin manifiesto no se puede comparar, y adivinar seria peor que callar.
|
|
310
|
+
return "";
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Aviso si la extension va por detras. Cadena vacia si todo cuadra o no se sabe.
|
|
315
|
+
*
|
|
316
|
+
* Solo se avisa UNA vez por sesion: repetirlo en cada accion seria ruido que
|
|
317
|
+
* acaba ignorandose, que es como se pierden los avisos que importan.
|
|
318
|
+
*/
|
|
319
|
+
function avisoDeVersion(deLaExtension) {
|
|
320
|
+
if (yaAvisadoDeVersion)
|
|
321
|
+
return "";
|
|
322
|
+
const esperada = versionEsperadaDeExtension();
|
|
323
|
+
if (!esperada || !deLaExtension || esperada === deLaExtension)
|
|
324
|
+
return "";
|
|
325
|
+
yaAvisadoDeVersion = true;
|
|
326
|
+
return (`\n\nAVISO: tu extension de Chrome es la v${deLaExtension} y este Chocolatito trae la v${esperada}. ` +
|
|
327
|
+
`Chrome sigue ejecutando la que cargo en su dia aunque los archivos del disco ya sean nuevos. ` +
|
|
328
|
+
`Recargala en chrome://extensions (boton de recargar en la tarjeta de Chocolatito Code) o ` +
|
|
329
|
+
`veras fallos raros que no se parecen a su causa.`);
|
|
330
|
+
}
|
|
281
331
|
class CatalogoDeRefs {
|
|
282
332
|
/** Lo que vio el modelo: los numeros en los que habla. */
|
|
283
333
|
vista = [];
|
|
@@ -315,6 +365,10 @@ class CatalogoDeRefs {
|
|
|
315
365
|
return this.filtroVista;
|
|
316
366
|
}
|
|
317
367
|
/** Nombre que tenia ese ref cuando el modelo lo vio. */
|
|
368
|
+
/** Huella de lo que vio el modelo, para poder comparar despues de actuar. */
|
|
369
|
+
huellaVista() {
|
|
370
|
+
return this.vista.map((e) => `${e.role}::${e.name}`);
|
|
371
|
+
}
|
|
318
372
|
nombreDe(ref) {
|
|
319
373
|
return this.vista.find((e) => e.ref === ref)?.name || "";
|
|
320
374
|
}
|
|
@@ -353,6 +407,61 @@ async function refrescarCatalogo(filtro) {
|
|
|
353
407
|
catalogo.verInterno(fresh.data.elements || []);
|
|
354
408
|
return true;
|
|
355
409
|
}
|
|
410
|
+
/**
|
|
411
|
+
* Comprobar que el clic hizo ALGO, en vez de dar por hecho que si.
|
|
412
|
+
*
|
|
413
|
+
* EL FALLO, VISTO EN UNA SESION REAL
|
|
414
|
+
*
|
|
415
|
+
* En Google Flow, el agente pulso el boton de generar y la herramienta contesto:
|
|
416
|
+
*
|
|
417
|
+
* Pulsado [19] "Iniciar generación". Pagina ahora: "Google Flow: sept 06..."
|
|
418
|
+
*
|
|
419
|
+
* El modelo leyo eso como "hecho" y se puso a esperar el resultado. Pero el clic
|
|
420
|
+
* no habia disparado nada: la pagina seguia igual. Espero, no vio nada, volvio a
|
|
421
|
+
* pulsar, volvio a esperar, gasto una captura de pantalla, y solo se desatasco
|
|
422
|
+
* cuando el usuario le dijo a mano "dale al boton de la flechita".
|
|
423
|
+
*
|
|
424
|
+
* El problema no fue del modelo: la herramienta le habia dicho que el clic se
|
|
425
|
+
* habia dado. Y "se envio el evento" no es lo mismo que "la pagina reacciono".
|
|
426
|
+
* Una web puede ignorar un clic sintetico por mil motivos -el manejador esta en
|
|
427
|
+
* otro elemento, hay un overlay delante, el framework escucha pointerdown y no
|
|
428
|
+
* click-, y ninguno de esos se ve desde fuera.
|
|
429
|
+
*
|
|
430
|
+
* QUE SE HACE AHORA
|
|
431
|
+
*
|
|
432
|
+
* Se mira la pagina antes y despues. Si no cambio absolutamente nada, se dice —
|
|
433
|
+
* que es informacion util y no un error: significa "el evento salio pero nadie
|
|
434
|
+
* lo recogio, prueba otra cosa". Es la diferencia entre que el agente se quede
|
|
435
|
+
* quince segundos esperando algo que no va a pasar, y que cambie de estrategia
|
|
436
|
+
* en el turno siguiente.
|
|
437
|
+
*/
|
|
438
|
+
async function reaccionAlClic(filtro, antes) {
|
|
439
|
+
let fresh;
|
|
440
|
+
try {
|
|
441
|
+
fresh = await extensionBridge.send("snapshot", { filter: filtro || "", max: 200 });
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
return "";
|
|
445
|
+
}
|
|
446
|
+
if (!fresh?.ok)
|
|
447
|
+
return "";
|
|
448
|
+
const els = fresh.data.elements || [];
|
|
449
|
+
catalogo.verInterno(els);
|
|
450
|
+
const ahora = els.map((e) => `${e.role}::${e.name}`);
|
|
451
|
+
const nuevos = ahora.filter((k) => !antes.includes(k));
|
|
452
|
+
const idos = antes.filter((k) => !ahora.includes(k));
|
|
453
|
+
if (nuevos.length === 0 && idos.length === 0) {
|
|
454
|
+
return (`\nOJO: el clic se envio, pero la pagina NO cambio en nada. Eso suele significar que ` +
|
|
455
|
+
`el elemento pulsado no es el que dispara la accion (el manejador esta en otro, hay algo ` +
|
|
456
|
+
`delante, o el framework escucha otro evento). No esperes un resultado: haz snapshot y ` +
|
|
457
|
+
`busca el control de verdad, o usa computer_use para un clic real del sistema.`);
|
|
458
|
+
}
|
|
459
|
+
const resumen = [
|
|
460
|
+
...nuevos.slice(0, 6).map((k) => ` + ${k.replace("::", " ")}`),
|
|
461
|
+
...idos.slice(0, 4).map((k) => ` - ${k.replace("::", " ")} (ya no esta)`),
|
|
462
|
+
].join("\n");
|
|
463
|
+
return `\nLa pagina reacciono (${nuevos.length} nuevos, ${idos.length} fuera):\n${resumen}`;
|
|
464
|
+
}
|
|
356
465
|
/**
|
|
357
466
|
* Ejecuta una accion sobre un ref del snapshot del modelo, traduciendolo al
|
|
358
467
|
* numero que la extension tiene vivo ahora mismo. Si el ref ha caducado (el
|
|
@@ -421,7 +530,8 @@ export async function chromeExtension(params, cwd = process.cwd(), apiKey) {
|
|
|
421
530
|
`Si no la reconoces, revisala en chrome://extensions.`
|
|
422
531
|
: "";
|
|
423
532
|
return (`Extension conectada (v${res.data.version}), emparejada con ${origin}. ` +
|
|
424
|
-
`Puedo trabajar en tus pestanas en segundo plano, sin robarte el foco.${aviso}`
|
|
533
|
+
`Puedo trabajar en tus pestanas en segundo plano, sin robarte el foco.${aviso}` +
|
|
534
|
+
avisoDeVersion(String(res.data.version || "")));
|
|
425
535
|
}
|
|
426
536
|
case "tabs": {
|
|
427
537
|
const res = await extensionBridge.send("tabs");
|
|
@@ -500,6 +610,9 @@ export async function chromeExtension(params, cwd = process.cwd(), apiKey) {
|
|
|
500
610
|
case "click": {
|
|
501
611
|
if (params.ref === undefined)
|
|
502
612
|
return fail(action, 'indica el "ref" del snapshot.');
|
|
613
|
+
// Se apunta como estaba la pagina ANTES: sin eso no hay con que comparar
|
|
614
|
+
// despues, y "se envio el clic" seguiria pasando por "la pagina reacciono".
|
|
615
|
+
const antesDelClic = catalogo.huellaVista();
|
|
503
616
|
const res = await accionSobreRef("click", {}, params.ref, params.filter);
|
|
504
617
|
if (!res.ok)
|
|
505
618
|
return fail(action, res.error);
|
|
@@ -512,7 +625,9 @@ export async function chromeExtension(params, cwd = process.cwd(), apiKey) {
|
|
|
512
625
|
const aviso = mismoElemento(pedido, pulsado)
|
|
513
626
|
? ""
|
|
514
627
|
: `\nAVISO: pediste "${pedido}" y se pulso "${pulsado}". Haz snapshot y comprueba en que estado quedo la pagina.`;
|
|
515
|
-
|
|
628
|
+
// Y lo que de verdad importa: si la pagina hizo algo o no. Ver reaccionAlClic.
|
|
629
|
+
const reaccion = antesDelClic.length > 0 ? await reaccionAlClic(params.filter || "", antesDelClic) : "";
|
|
630
|
+
return (`Pulsado [${params.ref}] "${pulsado}". Pagina ahora: "${res.data.title}" (${res.data.url})${aviso}${reaccion}`);
|
|
516
631
|
}
|
|
517
632
|
case "type": {
|
|
518
633
|
if (params.ref === undefined)
|
|
@@ -651,7 +766,7 @@ export async function chromeExtension(params, cwd = process.cwd(), apiKey) {
|
|
|
651
766
|
? path.isAbsolute(params.outputPath)
|
|
652
767
|
? params.outputPath
|
|
653
768
|
: path.resolve(cwd, params.outputPath)
|
|
654
|
-
: path.join(dir, "chrome_tab.
|
|
769
|
+
: path.join(dir, "chrome_tab.jpg");
|
|
655
770
|
const outDir = path.dirname(out);
|
|
656
771
|
if (!fs.existsSync(outDir))
|
|
657
772
|
fs.mkdirSync(outDir, { recursive: true });
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* "background": se actua sobre una ventana concreta sin traerla al frente y
|
|
24
24
|
* sin mover el raton del usuario.
|
|
25
25
|
*/
|
|
26
|
-
export type ComputerAction = "screenshot" | "ui_snapshot" | "ui_click" | "ui_type" | "ui_focus" | "find_element" | "left_click" | "click" | "double_click" | "triple_click" | "right_click" | "middle_click" | "mouse_move" | "move" | "left_click_drag" | "scroll" | "type" | "key" | "hotkey" | "wait" | "sleep" | "cursor_position" | "list_windows" | "focus_window" | "get_active_window" | "open_app";
|
|
26
|
+
export type ComputerAction = "screenshot" | "ui_snapshot" | "ui_click" | "ui_type" | "ui_focus" | "find_element" | "left_click" | "click" | "double_click" | "triple_click" | "right_click" | "middle_click" | "mouse_move" | "move" | "left_click_drag" | "scroll" | "type" | "key" | "hotkey" | "wait" | "wait_change" | "sleep" | "cursor_position" | "list_windows" | "focus_window" | "get_active_window" | "open_app";
|
|
27
27
|
export interface ComputerUseParams {
|
|
28
28
|
action: ComputerAction;
|
|
29
29
|
/** Ventana objetivo por titulo o proceso (ej. "chrome", "Flow"). */
|