chocolatito-code 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -0
- package/dist/agent/compaction.d.ts +65 -0
- package/dist/agent/compaction.js +134 -0
- package/dist/agent/context.d.ts +3 -0
- package/dist/agent/context.js +35 -0
- package/dist/agent/loop.d.ts +91 -2
- package/dist/agent/loop.js +642 -91
- package/dist/agent/loopDetector.d.ts +57 -0
- package/dist/agent/loopDetector.js +0 -0
- package/dist/agent/mentions.d.ts +13 -0
- package/dist/agent/mentions.js +49 -0
- package/dist/agent/repoMap.d.ts +117 -0
- package/dist/agent/repoMap.js +560 -0
- package/dist/agent/result.d.ts +64 -0
- package/dist/agent/result.js +48 -0
- package/dist/agent/retry.d.ts +35 -0
- package/dist/agent/retry.js +123 -4
- package/dist/agent/salidaDelAgente.d.ts +43 -0
- package/dist/agent/salidaDelAgente.js +53 -0
- package/dist/agent/subagent.js +62 -15
- package/dist/agent/syntaxValidator.js +288 -52
- package/dist/agent/toolGate.d.ts +58 -0
- package/dist/agent/toolGate.js +111 -0
- package/dist/agent/tracker.js +4 -7
- package/dist/agent/undoManager.d.ts +44 -11
- package/dist/agent/undoManager.js +95 -24
- package/dist/agent/usageMeter.d.ts +56 -0
- package/dist/agent/usageMeter.js +51 -0
- package/dist/agent/verifier.d.ts +49 -0
- package/dist/agent/verifier.js +158 -0
- package/dist/config/constants.js +14 -0
- package/dist/config/engine.d.ts +19 -0
- package/dist/config/engine.js +35 -3
- package/dist/config/license.d.ts +5 -0
- package/dist/config/license.js +107 -9
- package/dist/config/limits.d.ts +28 -0
- package/dist/config/limits.js +51 -0
- package/dist/config/nodeVersion.d.ts +13 -0
- package/dist/config/nodeVersion.js +26 -0
- package/dist/config/permissions.d.ts +76 -1
- package/dist/config/permissions.js +379 -23
- package/dist/config/quota.js +24 -1
- package/dist/config/updater.d.ts +68 -0
- package/dist/config/updater.js +215 -0
- package/dist/hooks/manager.js +159 -14
- package/dist/index.js +240 -37
- package/dist/mcp/client.d.ts +50 -0
- package/dist/mcp/client.js +155 -19
- package/dist/mcp/ide.d.ts +76 -0
- package/dist/mcp/ide.js +160 -0
- package/dist/mcp/manager.js +9 -0
- package/dist/prompts/systemPrompt.js +12 -1
- package/dist/sessions/manager.d.ts +12 -0
- package/dist/sessions/manager.js +33 -5
- package/dist/sessions/resume.d.ts +36 -0
- package/dist/sessions/resume.js +43 -0
- package/dist/skills/manager.d.ts +1 -0
- package/dist/skills/manager.js +9 -0
- package/dist/tools/binary.d.ts +46 -0
- package/dist/tools/binary.js +100 -0
- package/dist/tools/browserCdp.js +13 -1
- package/dist/tools/browserExtension.d.ts +5 -0
- package/dist/tools/browserExtension.js +237 -22
- package/dist/tools/browserSession.d.ts +23 -0
- package/dist/tools/browserSession.js +28 -0
- package/dist/tools/computerUse.js +33 -1
- package/dist/tools/createImage.d.ts +11 -0
- package/dist/tools/createImage.js +13 -1
- package/dist/tools/definitions.js +17 -4
- package/dist/tools/editDiagnosis.d.ts +54 -0
- package/dist/tools/editDiagnosis.js +142 -0
- package/dist/tools/editFile.d.ts +1 -0
- package/dist/tools/editFile.js +118 -62
- package/dist/tools/findFiles.d.ts +21 -0
- package/dist/tools/findFiles.js +75 -6
- package/dist/tools/getSystemInfo.js +15 -7
- package/dist/tools/gitTools.d.ts +24 -0
- package/dist/tools/gitTools.js +87 -26
- package/dist/tools/grepSearch.js +111 -23
- package/dist/tools/moveCopyFile.d.ts +18 -1
- package/dist/tools/moveCopyFile.js +74 -4
- package/dist/tools/patchCascade.d.ts +57 -0
- package/dist/tools/patchCascade.js +338 -0
- package/dist/tools/runCommand.js +213 -25
- package/dist/tools/runner.js +58 -5
- package/dist/tools/toolDefsComputer.js +12 -2
- package/dist/tools/truncator.d.ts +27 -1
- package/dist/tools/truncator.js +117 -11
- package/dist/tools/viewFile.js +15 -11
- package/dist/tools/visionBridge.js +7 -0
- package/dist/tools/webSearch.d.ts +21 -0
- package/dist/tools/webSearch.js +125 -14
- package/dist/tools/writeFile.d.ts +16 -1
- package/dist/tools/writeFile.js +40 -2
- package/dist/ui/interrupt.d.ts +4 -1
- package/dist/ui/interrupt.js +59 -7
- package/dist/ui/keyboardGuard.d.ts +76 -0
- package/dist/ui/keyboardGuard.js +94 -0
- package/dist/ui/loopPrompt.d.ts +17 -0
- package/dist/ui/loopPrompt.js +59 -0
- package/dist/ui/pegados.d.ts +50 -0
- package/dist/ui/pegados.js +83 -0
- package/dist/ui/permissionPrompt.js +84 -2
- package/dist/ui/prompt.d.ts +40 -0
- package/dist/ui/prompt.js +436 -131
- package/dist/ui/reasoningStream.d.ts +8 -0
- package/dist/ui/reasoningStream.js +99 -7
- package/dist/ui/salida.d.ts +10 -0
- package/dist/ui/salida.js +50 -0
- package/extension/README.md +95 -74
- package/extension/background.js +772 -181
- package/extension/content.js +527 -46
- package/extension/fuentes/Chocolatito-Marca.ttf +0 -0
- package/extension/iconos/128.png +0 -0
- package/extension/iconos/16.png +0 -0
- package/extension/iconos/32.png +0 -0
- package/extension/iconos/48.png +0 -0
- package/extension/iconos/logotipo.png +0 -0
- package/extension/manifest.json +28 -4
- package/extension/popup.html +16 -6
- package/extension/popup.js +28 -12
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -41,6 +41,99 @@ Para cambiar de nivel dentro de la sesión, escribe:
|
|
|
41
41
|
|
|
42
42
|
Las lecturas independientes lanzadas en el mismo turno se ejecutan **en paralelo**.
|
|
43
43
|
|
|
44
|
+
## 🗺️ Mapa del repositorio
|
|
45
|
+
|
|
46
|
+
Al arrancar, el agente recibe un índice de los símbolos del proyecto ordenado por **cuánto los
|
|
47
|
+
usa el resto del código** (un PageRank sobre el grafo de referencias, con presupuesto de 4.000
|
|
48
|
+
tokens). Sirve para que sepa qué existe y dónde mirar sin gastar turnos en `grep_search`
|
|
49
|
+
buscando a ciegas.
|
|
50
|
+
|
|
51
|
+
Son nombres y números de línea, no contenido: antes de editar sigue leyendo el archivo.
|
|
52
|
+
|
|
53
|
+
Lo que escribes también cuenta. Si dices *"arregla calcularImpuesto"* o *"falla en loop.ts"*, se
|
|
54
|
+
detecta la referencia aunque no pongas `@` y se le pasan las rutas —solo las rutas, no el
|
|
55
|
+
contenido, que cuesta más contexto del que ahorra.
|
|
56
|
+
|
|
57
|
+
## ✅ Verificación automática tras editar
|
|
58
|
+
|
|
59
|
+
Cuando un turno escribe en disco, Chocolatito puede correr el verificador del proyecto y
|
|
60
|
+
devolverle el error al agente **en el mismo turno**, en vez de dar el trabajo por bueno y que el
|
|
61
|
+
fallo aparezca tres turnos después.
|
|
62
|
+
|
|
63
|
+
Viene **apagada**: correr un comando del proyecto es ejecutar código, y eso se pide. Para
|
|
64
|
+
activarla, en `.chocolatito/settings.json` (del proyecto o global en `~/.chocolatito/`):
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"verify": { "auto": true },
|
|
69
|
+
"limits": { "maxIterations": 60, "subagentIterations": 20 }
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
También con variables de entorno: `CHOCOLATITO_AUTO_VERIFY=1`, `CHOCOLATITO_VERIFY_CMD`,
|
|
74
|
+
`CHOCOLATITO_MAX_ITERATIONS`, `CHOCOLATITO_SUBAGENT_ITERATIONS`.
|
|
75
|
+
|
|
76
|
+
El comando se detecta solo, en este orden: `npm run typecheck` → `lint` → `check` → `build`, o
|
|
77
|
+
`npx tsc --noEmit`, `cargo check`, `go build ./...`. **Nunca los tests**: un `npm test` puede
|
|
78
|
+
levantar servidores o tardar minutos, y verificar no puede significar eso. Con `verify.command`
|
|
79
|
+
pones el tuyo. Si falla, el agente tiene tres intentos para arreglarlo; al cuarto para y te lo
|
|
80
|
+
dice.
|
|
81
|
+
|
|
82
|
+
## ↻ Cuando el agente se atasca
|
|
83
|
+
|
|
84
|
+
Si repite tres veces la misma llamada con el mismo resultado, se para y pregunta: le dejas
|
|
85
|
+
seguir, o le dices que cambie de enfoque. Sin terminal (CI, `chocolatito "haz X"`) se corta
|
|
86
|
+
solo, para que un bucle que nadie está mirando no se coma la cuota.
|
|
87
|
+
|
|
88
|
+
## 🧩 VSCode
|
|
89
|
+
|
|
90
|
+
Si tienes la extensión instalada y abres `chocolatito` en la terminal integrada, **se conecta
|
|
91
|
+
solo**. No hay nada que configurar.
|
|
92
|
+
|
|
93
|
+
El truco está al revés de lo que parece: la extensión es un **servidor MCP** y el agente es el
|
|
94
|
+
cliente. Cada ventana de VSCode levanta un servidor en `127.0.0.1` y deja una ficha en
|
|
95
|
+
`~/.chocolatito/ide/` con su puerto, su token y la carpeta que tiene abierta. El agente busca la
|
|
96
|
+
ficha cuya carpeta contiene su directorio de trabajo.
|
|
97
|
+
|
|
98
|
+
Lo que gana el agente (lo único que no puede tener por su cuenta):
|
|
99
|
+
|
|
100
|
+
| Herramienta | Para qué |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| `diagnosticos` | Los errores del servidor de lenguaje. Los subrayados rojos, sin esperar a compilar. |
|
|
103
|
+
| `seleccion_actual` | Qué archivo tienes abierto y qué has seleccionado. Hace que «arregla esto» signifique algo. |
|
|
104
|
+
| `archivos_abiertos` | Las pestañas abiertas, en orden. |
|
|
105
|
+
| `mostrar_diff` | Abre el visor de diferencias nativo. No escribe nada: es para que lo veas antes. |
|
|
106
|
+
|
|
107
|
+
**Con varias ventanas abiertas se elige la correcta**, no la primera: gana la carpeta más
|
|
108
|
+
específica que contenga el directorio de trabajo, y si ninguna lo contiene **no se conecta a
|
|
109
|
+
nada**. Conectarse a la ventana de otro proyecto sería peor que no conectarse: los diffs saldrían
|
|
110
|
+
en el sitio equivocado.
|
|
111
|
+
|
|
112
|
+
El servidor escucha solo en `127.0.0.1` y exige un token de 32 bytes que va en la ficha, dentro de
|
|
113
|
+
tu carpeta personal. Sin eso, cualquier programa de la máquina podría manejar tu editor.
|
|
114
|
+
|
|
115
|
+
Para apagarlo: `CHOCOLATITO_SIN_EDITOR=1`, o la opción `chocolatito.puente` en VSCode.
|
|
116
|
+
|
|
117
|
+
### Instalarla
|
|
118
|
+
|
|
119
|
+
Mientras no esté en el marketplace, se instala desde el paquete:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
cd extension-vscode
|
|
123
|
+
npx @vscode/vsce package --allow-missing-repository --skip-license
|
|
124
|
+
code --install-extension chocolatito-code-vscode-0.1.1.vsix
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Con VSCode cerrado**, y reiniciándolo después. Copiar la carpeta a mano a `~/.vscode/extensions`
|
|
128
|
+
**no funciona**: falta el `.vsixmanifest` y VSCode la lista pero no la carga nunca, sin dar ningún
|
|
129
|
+
error. Para desarrollar, `F5` sobre la carpeta abre una ventana con la extensión cargada al vuelo.
|
|
130
|
+
|
|
131
|
+
En la barra de estado, abajo a la derecha, pone **Chocolatito** con el estado del puente. Si algo
|
|
132
|
+
falla —sin carpeta abierta, puente apagado, puerto ocupado— lo dice ahí, y al pulsarlo se abre el
|
|
133
|
+
detalle.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
44
137
|
## 🔌 MCP (Model Context Protocol)
|
|
45
138
|
|
|
46
139
|
Conecta cualquier servidor MCP y sus herramientas quedan disponibles para el agente.
|
|
@@ -97,6 +190,23 @@ solo lectura de verdad: ni comandos, ni clics, ni escrituras.
|
|
|
97
190
|
|
|
98
191
|
`--yes` aprueba todo sin preguntar: es para CI, no para el día a día.
|
|
99
192
|
|
|
193
|
+
## 📋 Pegar texto largo
|
|
194
|
+
|
|
195
|
+
Pegar un log de 300 líneas ya no mete 300 líneas en el recuadro. Se guarda aparte y en el prompt
|
|
196
|
+
queda una marca corta:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
❯ arregla esto [pegado #1 · 347 lineas]
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**El modelo recibe el texto entero**, no la marca: al enviar se sustituye por lo que pegaste. Y un
|
|
203
|
+
solo `backspace` con el cursor detrás de la marca se la lleva completa — lo que ocupa una línea se
|
|
204
|
+
borra como una línea.
|
|
205
|
+
|
|
206
|
+
Por debajo de 5 líneas o 400 caracteres se pega normal, entero y a la vista.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
100
210
|
## ⌨️ Escribir mientras trabaja
|
|
101
211
|
|
|
102
212
|
No hace falta esperar a que termine. Lo que escribas durante una generación aparece bajo
|
|
@@ -150,6 +260,25 @@ Para desarrollar sobre el propio proyecto:
|
|
|
150
260
|
npm install && npm run build && npm link
|
|
151
261
|
```
|
|
152
262
|
|
|
263
|
+
### Actualizaciones
|
|
264
|
+
|
|
265
|
+
Se instalan solas. Al arrancar se le pregunta al registro de npm si hay una versión más
|
|
266
|
+
nueva (como mucho una vez cada 6 horas, y sin retrasar el arranque: si no hay red, no pasa
|
|
267
|
+
nada). Si la hay, se instala de fondo y entra **al reiniciar** `chocolatito` — nunca a
|
|
268
|
+
mitad de sesión.
|
|
269
|
+
|
|
270
|
+
| | |
|
|
271
|
+
| --- | --- |
|
|
272
|
+
| `/actualizar` | Comprobar e instalar ahora, sin esperar a la siguiente ventana. |
|
|
273
|
+
| `CHOCOLATITO_SIN_ACTUALIZAR=1` | Apagarlo del todo. |
|
|
274
|
+
|
|
275
|
+
Una instalación de desarrollo (`npm link` sobre el repositorio) **no se toca nunca**:
|
|
276
|
+
instalar encima le borraría el trabajo a quien lo está escribiendo. Y no se baja jamás a
|
|
277
|
+
una preversión, aunque el registro la anuncie como la última.
|
|
278
|
+
|
|
279
|
+
Si la carpeta global es de administrador y no hay permiso para escribir, no se insiste: se
|
|
280
|
+
dice una vez el comando (`npm i -g chocolatito-code@latest`) y se sigue trabajando.
|
|
281
|
+
|
|
153
282
|
---
|
|
154
283
|
|
|
155
284
|
## 🚀 Uso Rápido
|
|
@@ -169,6 +298,32 @@ chocolatito --help
|
|
|
169
298
|
chocolatito --version
|
|
170
299
|
```
|
|
171
300
|
|
|
301
|
+
### Códigos de salida
|
|
302
|
+
|
|
303
|
+
En modo orden única el código dice qué pasó de verdad. Antes siempre era `0`: una cuota
|
|
304
|
+
agotada o una licencia caída dejaban el paso del CI en verde sin haber tocado nada.
|
|
305
|
+
|
|
306
|
+
| Código | Qué significa | Qué hacer |
|
|
307
|
+
| :---: | :--- | :--- |
|
|
308
|
+
| `0` | La tarea se completó. | Nada. |
|
|
309
|
+
| `1` | El agente no pudo terminar: el motor falló tras los reintentos, o se agotó el límite de 30 iteraciones. | Reintentar tiene sentido. |
|
|
310
|
+
| `2` | Error de uso: no se dio ninguna orden, o no hay terminal para pedirla. | Arreglar la invocación. |
|
|
311
|
+
| `3` | Cuota agotada. | Esperar a que se libere la ventana; reintentar no la devuelve. |
|
|
312
|
+
| `4` | Licencia o API key no válida. | Revisar `/licencia` o `~/.chocolatitorc`. |
|
|
313
|
+
| `5` | Ya hay 4 peticiones en curso con esta licencia. | Esperar los segundos que dice el mensaje y reintentar. |
|
|
314
|
+
| `130` | Interrumpido con `Esc` (128 + SIGINT, como cualquier otro programa). | Nada. |
|
|
315
|
+
|
|
316
|
+
Lo que no es `0` se explica también por `stderr`, con el motivo del corte. Un ejemplo:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
chocolatito --yes "ejecuta los tests y arregla lo que falle"
|
|
320
|
+
case $? in
|
|
321
|
+
0) echo "hecho" ;;
|
|
322
|
+
3|4) echo "problema de cuenta, no de código: no reintentes" ;;
|
|
323
|
+
*) echo "falló, se puede reintentar" ;;
|
|
324
|
+
esac
|
|
325
|
+
```
|
|
326
|
+
|
|
172
327
|
## 🧪 Pruebas
|
|
173
328
|
|
|
174
329
|
```bash
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type OpenAI from "openai";
|
|
2
|
+
/**
|
|
3
|
+
* Recorte del historial sin romper el emparejamiento llamada-respuesta.
|
|
4
|
+
*
|
|
5
|
+
* La compactacion anterior cortaba por posicion fija: `slice(1, -4)` para lo que
|
|
6
|
+
* se resumia y `slice(-4)` para lo que se conservaba. Un turno con cuatro
|
|
7
|
+
* herramientas deja el historial acabado en [assistant(tool_calls), tool, tool,
|
|
8
|
+
* tool, tool]; ahi `slice(-4)` empieza por un `tool` y se queda con cuatro
|
|
9
|
+
* respuestas cuya llamada se ha quedado en la otra mitad. La API responde 400
|
|
10
|
+
* ("tool message without preceding tool_calls") y, como la compactacion vive
|
|
11
|
+
* dentro de un try/catch mudo, el usuario solo veia que la sesion se rompia
|
|
12
|
+
* justo cuando llevaba mucho rato trabajando.
|
|
13
|
+
*
|
|
14
|
+
* Aqui todo se decide por la forma del historial, no por un numero de mensajes.
|
|
15
|
+
*/
|
|
16
|
+
export type Mensaje = OpenAI.Chat.ChatCompletionMessageParam;
|
|
17
|
+
/** Un assistant que pide herramientas: obliga a que le sigan sus respuestas. */
|
|
18
|
+
export declare function pideHerramientas(m: Mensaje | undefined): boolean;
|
|
19
|
+
/** Una respuesta de herramienta: no puede existir sin su llamada delante. */
|
|
20
|
+
export declare function esResultado(m: Mensaje | undefined): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Un corte en el indice `i` es valido si no parte un bloque de herramientas:
|
|
23
|
+
* ni deja una respuesta sin su llamada delante, ni una llamada sin respuestas
|
|
24
|
+
* detras. El indice 0 nunca se toca: es el prompt de sistema.
|
|
25
|
+
*/
|
|
26
|
+
export declare function esCorteValido(mensajes: Mensaje[], i: number): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Indice donde empieza la cola que se conserva.
|
|
29
|
+
*
|
|
30
|
+
* Se apunta a dejar `colaMinima` mensajes recientes y, si ese punto parte un
|
|
31
|
+
* bloque, se retrocede hasta el primer corte limpio (conservar de mas es
|
|
32
|
+
* gratis). Solo si retrocediendo no queda nada que resumir se avanza, que es
|
|
33
|
+
* peor pero sigue siendo valido.
|
|
34
|
+
*/
|
|
35
|
+
export declare function corteSeguro(mensajes: Mensaje[], colaMinima: number): number;
|
|
36
|
+
/**
|
|
37
|
+
* Deja un tramo del historial en un estado que la API acepte por si solo.
|
|
38
|
+
*
|
|
39
|
+
* Sirve para el trozo que se manda a resumir: alli las respuestas de
|
|
40
|
+
* herramientas cuya llamada quedo fuera del tramo son basura que provoca un 400,
|
|
41
|
+
* y una llamada cuyas respuestas quedaron fuera, tambien.
|
|
42
|
+
*/
|
|
43
|
+
export declare function limpiarHuerfanos(tramo: Mensaje[]): Mensaje[];
|
|
44
|
+
/** Marca que sustituye a una salida de herramienta ya inservible. */
|
|
45
|
+
export declare const SALIDA_DESCARTADA = "[salida descartada al liberar contexto; vuelve a ejecutar la herramienta si necesitas el detalle]";
|
|
46
|
+
/**
|
|
47
|
+
* Vacia las salidas de herramienta mas viejas conservando el mensaje.
|
|
48
|
+
*
|
|
49
|
+
* Es la mitad del historial en cualquier sesion larga (un `view_file` de hace
|
|
50
|
+
* veinte pasos ocupa miles de tokens y ya no dice nada), y borrarlo no cuesta
|
|
51
|
+
* ni una llamada al modelo ni pierde ninguna decision tomada. El mensaje se
|
|
52
|
+
* queda en su sitio con un texto corto: quitarlo dejaria huerfana su llamada.
|
|
53
|
+
*/
|
|
54
|
+
export declare function vaciarResultadosAntiguos(mensajes: Mensaje[], conservarUltimos?: number, minimoCaracteres?: number): {
|
|
55
|
+
mensajes: Mensaje[];
|
|
56
|
+
liberados: number;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Tamano aproximado del historial en tokens.
|
|
60
|
+
*
|
|
61
|
+
* No hay tokenizador a mano, asi que se cuenta por caracteres. Sirve para lo
|
|
62
|
+
* unico que se le pide: comparar el historial consigo mismo antes y despues de
|
|
63
|
+
* podarlo. La cifra absoluta buena la da la API en `prompt_tokens`.
|
|
64
|
+
*/
|
|
65
|
+
export declare function estimarTokens(mensajes: Mensaje[]): number;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/** Un assistant que pide herramientas: obliga a que le sigan sus respuestas. */
|
|
2
|
+
export function pideHerramientas(m) {
|
|
3
|
+
if (!m || m.role !== "assistant")
|
|
4
|
+
return false;
|
|
5
|
+
const tc = m.tool_calls;
|
|
6
|
+
return Array.isArray(tc) && tc.length > 0;
|
|
7
|
+
}
|
|
8
|
+
/** Una respuesta de herramienta: no puede existir sin su llamada delante. */
|
|
9
|
+
export function esResultado(m) {
|
|
10
|
+
return !!m && m.role === "tool";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Un corte en el indice `i` es valido si no parte un bloque de herramientas:
|
|
14
|
+
* ni deja una respuesta sin su llamada delante, ni una llamada sin respuestas
|
|
15
|
+
* detras. El indice 0 nunca se toca: es el prompt de sistema.
|
|
16
|
+
*/
|
|
17
|
+
export function esCorteValido(mensajes, i) {
|
|
18
|
+
if (i < 1 || i > mensajes.length)
|
|
19
|
+
return false;
|
|
20
|
+
if (esResultado(mensajes[i]))
|
|
21
|
+
return false;
|
|
22
|
+
if (pideHerramientas(mensajes[i - 1]))
|
|
23
|
+
return false;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Indice donde empieza la cola que se conserva.
|
|
28
|
+
*
|
|
29
|
+
* Se apunta a dejar `colaMinima` mensajes recientes y, si ese punto parte un
|
|
30
|
+
* bloque, se retrocede hasta el primer corte limpio (conservar de mas es
|
|
31
|
+
* gratis). Solo si retrocediendo no queda nada que resumir se avanza, que es
|
|
32
|
+
* peor pero sigue siendo valido.
|
|
33
|
+
*/
|
|
34
|
+
export function corteSeguro(mensajes, colaMinima) {
|
|
35
|
+
const objetivo = Math.max(1, mensajes.length - Math.max(0, colaMinima));
|
|
36
|
+
for (let i = objetivo; i >= 1; i--) {
|
|
37
|
+
if (esCorteValido(mensajes, i))
|
|
38
|
+
return i;
|
|
39
|
+
}
|
|
40
|
+
for (let i = objetivo + 1; i <= mensajes.length; i++) {
|
|
41
|
+
if (esCorteValido(mensajes, i))
|
|
42
|
+
return i;
|
|
43
|
+
}
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Deja un tramo del historial en un estado que la API acepte por si solo.
|
|
48
|
+
*
|
|
49
|
+
* Sirve para el trozo que se manda a resumir: alli las respuestas de
|
|
50
|
+
* herramientas cuya llamada quedo fuera del tramo son basura que provoca un 400,
|
|
51
|
+
* y una llamada cuyas respuestas quedaron fuera, tambien.
|
|
52
|
+
*/
|
|
53
|
+
export function limpiarHuerfanos(tramo) {
|
|
54
|
+
const idsRespondidos = new Set();
|
|
55
|
+
for (const m of tramo) {
|
|
56
|
+
if (esResultado(m)) {
|
|
57
|
+
const id = m.tool_call_id;
|
|
58
|
+
if (id)
|
|
59
|
+
idsRespondidos.add(String(id));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const idsLlamados = new Set();
|
|
63
|
+
const salida = [];
|
|
64
|
+
for (const m of tramo) {
|
|
65
|
+
if (pideHerramientas(m)) {
|
|
66
|
+
const conRespuesta = m.tool_calls.filter((tc) => idsRespondidos.has(String(tc?.id)));
|
|
67
|
+
if (conRespuesta.length === 0) {
|
|
68
|
+
// Sin ninguna respuesta detras no aporta nada y rompe la peticion.
|
|
69
|
+
const texto = m.content;
|
|
70
|
+
if (typeof texto === "string" && texto.trim()) {
|
|
71
|
+
salida.push({ role: "assistant", content: texto });
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
conRespuesta.forEach((tc) => idsLlamados.add(String(tc.id)));
|
|
76
|
+
salida.push({ ...m, tool_calls: conRespuesta });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (esResultado(m)) {
|
|
80
|
+
const id = String(m.tool_call_id ?? "");
|
|
81
|
+
if (!idsLlamados.has(id))
|
|
82
|
+
continue;
|
|
83
|
+
salida.push(m);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
salida.push(m);
|
|
87
|
+
}
|
|
88
|
+
return salida;
|
|
89
|
+
}
|
|
90
|
+
/** Marca que sustituye a una salida de herramienta ya inservible. */
|
|
91
|
+
export const SALIDA_DESCARTADA = "[salida descartada al liberar contexto; vuelve a ejecutar la herramienta si necesitas el detalle]";
|
|
92
|
+
/**
|
|
93
|
+
* Vacia las salidas de herramienta mas viejas conservando el mensaje.
|
|
94
|
+
*
|
|
95
|
+
* Es la mitad del historial en cualquier sesion larga (un `view_file` de hace
|
|
96
|
+
* veinte pasos ocupa miles de tokens y ya no dice nada), y borrarlo no cuesta
|
|
97
|
+
* ni una llamada al modelo ni pierde ninguna decision tomada. El mensaje se
|
|
98
|
+
* queda en su sitio con un texto corto: quitarlo dejaria huerfana su llamada.
|
|
99
|
+
*/
|
|
100
|
+
export function vaciarResultadosAntiguos(mensajes, conservarUltimos = 12, minimoCaracteres = 400) {
|
|
101
|
+
const limite = mensajes.length - Math.max(0, conservarUltimos);
|
|
102
|
+
let liberados = 0;
|
|
103
|
+
const salida = mensajes.map((m, i) => {
|
|
104
|
+
if (i >= limite || !esResultado(m))
|
|
105
|
+
return m;
|
|
106
|
+
const contenido = m.content;
|
|
107
|
+
if (typeof contenido !== "string")
|
|
108
|
+
return m;
|
|
109
|
+
if (contenido.length < minimoCaracteres || contenido === SALIDA_DESCARTADA)
|
|
110
|
+
return m;
|
|
111
|
+
liberados += contenido.length - SALIDA_DESCARTADA.length;
|
|
112
|
+
return { ...m, content: SALIDA_DESCARTADA };
|
|
113
|
+
});
|
|
114
|
+
return { mensajes: salida, liberados };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Tamano aproximado del historial en tokens.
|
|
118
|
+
*
|
|
119
|
+
* No hay tokenizador a mano, asi que se cuenta por caracteres. Sirve para lo
|
|
120
|
+
* unico que se le pide: comparar el historial consigo mismo antes y despues de
|
|
121
|
+
* podarlo. La cifra absoluta buena la da la API en `prompt_tokens`.
|
|
122
|
+
*/
|
|
123
|
+
export function estimarTokens(mensajes) {
|
|
124
|
+
let caracteres = 0;
|
|
125
|
+
for (const m of mensajes) {
|
|
126
|
+
try {
|
|
127
|
+
caracteres += JSON.stringify(m)?.length || 0;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
caracteres += 0;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return Math.ceil(caracteres / 4);
|
|
134
|
+
}
|
package/dist/agent/context.d.ts
CHANGED
|
@@ -5,5 +5,8 @@ export interface ProjectContext {
|
|
|
5
5
|
gitBranch: string | null;
|
|
6
6
|
projectType: string;
|
|
7
7
|
availableScripts: string[];
|
|
8
|
+
/** Indice de simbolos del proyecto, ya rankeado y recortado. Ver repoMap.ts. */
|
|
9
|
+
repoMap: string;
|
|
8
10
|
}
|
|
11
|
+
export declare function olvidarMapa(cwd?: string): void;
|
|
9
12
|
export declare function getProjectContext(cwd?: string): Promise<ProjectContext>;
|
package/dist/agent/context.js
CHANGED
|
@@ -2,6 +2,40 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { execa } from "execa";
|
|
4
4
|
import { RULES_FILE_NAME } from "../config/constants.js";
|
|
5
|
+
import { construirMapa } from "./repoMap.js";
|
|
6
|
+
/** Tokens que puede ocupar el mapa dentro del prompt de sistema. */
|
|
7
|
+
const PRESUPUESTO_MAPA = 4_000;
|
|
8
|
+
/**
|
|
9
|
+
* El mapa se calcula una vez por directorio.
|
|
10
|
+
*
|
|
11
|
+
* getProjectContext se llama de nuevo en cada cambio de modelo, de modo y de
|
|
12
|
+
* directorio —hasta ocho veces en una sesion corta—, y recorrer el repositorio
|
|
13
|
+
* entero cada vez para obtener exactamente el mismo texto es tiempo regalado.
|
|
14
|
+
*/
|
|
15
|
+
const cacheDeMapas = new Map();
|
|
16
|
+
export function olvidarMapa(cwd) {
|
|
17
|
+
if (cwd)
|
|
18
|
+
cacheDeMapas.delete(path.resolve(cwd));
|
|
19
|
+
else
|
|
20
|
+
cacheDeMapas.clear();
|
|
21
|
+
}
|
|
22
|
+
function mapaDe(cwd) {
|
|
23
|
+
const clave = path.resolve(cwd);
|
|
24
|
+
const guardado = cacheDeMapas.get(clave);
|
|
25
|
+
if (guardado !== undefined)
|
|
26
|
+
return guardado;
|
|
27
|
+
let mapa = "";
|
|
28
|
+
try {
|
|
29
|
+
mapa = construirMapa({ cwd: clave, presupuesto: PRESUPUESTO_MAPA });
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Un proyecto raro no puede impedir que arranque el agente: sin mapa se
|
|
33
|
+
// trabaja igual, solo que a ciegas, que es como se trabajaba antes.
|
|
34
|
+
mapa = "";
|
|
35
|
+
}
|
|
36
|
+
cacheDeMapas.set(clave, mapa);
|
|
37
|
+
return mapa;
|
|
38
|
+
}
|
|
5
39
|
export async function getProjectContext(cwd = process.cwd()) {
|
|
6
40
|
let rulesContent = null;
|
|
7
41
|
// 1. Look for CHOCOLATITO.md or CLAUDE.md
|
|
@@ -119,5 +153,6 @@ export async function getProjectContext(cwd = process.cwd()) {
|
|
|
119
153
|
gitBranch,
|
|
120
154
|
projectType,
|
|
121
155
|
availableScripts,
|
|
156
|
+
repoMap: mapaDe(cwd),
|
|
122
157
|
};
|
|
123
158
|
}
|
package/dist/agent/loop.d.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
2
|
import { TokenTracker } from "./tracker.js";
|
|
3
3
|
import { EspectroModel } from "../config/constants.js";
|
|
4
|
+
import { ResultadoTarea } from "./result.js";
|
|
4
5
|
import { PermissionManager } from "../config/permissions.js";
|
|
5
6
|
import { HookManager } from "../hooks/manager.js";
|
|
6
7
|
export declare class AgentLoop {
|
|
7
8
|
private client;
|
|
9
|
+
/**
|
|
10
|
+
* Cliente aparte para resumir el historial. No es cosmetico: la cabecera
|
|
11
|
+
* X-Chocolatito-Route es lo que le permite al proxy saber en que se le va la
|
|
12
|
+
* cuota al usuario, y con un unico cliente las compactaciones —que son las
|
|
13
|
+
* llamadas mas caras que hace el agente— llegaban etiquetadas como "loop".
|
|
14
|
+
*/
|
|
15
|
+
private compactClient;
|
|
8
16
|
private currentEspectro;
|
|
9
17
|
messages: OpenAI.Chat.ChatCompletionMessageParam[];
|
|
10
18
|
tracker: TokenTracker;
|
|
@@ -14,12 +22,93 @@ export declare class AgentLoop {
|
|
|
14
22
|
private apiKey;
|
|
15
23
|
private baseURL;
|
|
16
24
|
private maxIterations;
|
|
25
|
+
/**
|
|
26
|
+
* Vigila que el agente no se quede dando vueltas. Vive en la instancia y no
|
|
27
|
+
* en runTask porque el estado tiene que sobrevivir a las iteraciones del
|
|
28
|
+
* while; se reinicia en cada turno nuevo.
|
|
29
|
+
*/
|
|
30
|
+
private detectorBucle;
|
|
31
|
+
/**
|
|
32
|
+
* Archivos escritos en el turno. Solo sirve para saber si hay algo que
|
|
33
|
+
* verificar al cerrarlo: si el turno no toco el disco, no hay nada que
|
|
34
|
+
* comprobar y correr el compilador seria tiempo del usuario tirado.
|
|
35
|
+
*/
|
|
36
|
+
private archivosTocados;
|
|
17
37
|
constructor(apiKey: string, baseURL: string, initialModel: EspectroModel, systemPrompt: string);
|
|
18
38
|
setEspectroModel(model: EspectroModel, updatedSystemPrompt?: string): void;
|
|
19
39
|
getEspectroModel(): EspectroModel;
|
|
20
40
|
clearHistory(systemPrompt: string): void;
|
|
21
41
|
compactHistory(systemPrompt: string): Promise<string>;
|
|
42
|
+
/**
|
|
43
|
+
* Tokens que OCUPA el historial ahora mismo, no los que se han gastado en el
|
|
44
|
+
* turno. Son dos numeros distintos y confundirlos era el fallo de abajo.
|
|
45
|
+
*/
|
|
22
46
|
lastTurnTokens: number;
|
|
23
|
-
|
|
24
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Compactacion automatica del contexto.
|
|
49
|
+
*
|
|
50
|
+
* Habia tres cosas mal, y cada una se comia el contexto por su lado:
|
|
51
|
+
*
|
|
52
|
+
* 1. El corte era por posicion fija: slice(1, -4) para lo que se resumia y
|
|
53
|
+
* slice(-4) para lo que se conservaba. Un turno que acaba en un bloque de
|
|
54
|
+
* cuatro herramientas deja la cola empezando por un mensaje "tool" cuya
|
|
55
|
+
* llamada se ha quedado en la mitad resumida. La API contesta 400 y el
|
|
56
|
+
* catch mudo lo tapaba: la sesion se rompia sin motivo visible, y siempre
|
|
57
|
+
* en las sesiones largas, que son las que llevan mas trabajo encima. El
|
|
58
|
+
* corte lo decide ahora la forma del historial, nunca una posicion.
|
|
59
|
+
*
|
|
60
|
+
* 2. El umbral solo se miraba al empezar el turno y sobre el dato del turno
|
|
61
|
+
* ANTERIOR. Un turno de veinte herramientas puede doblar el contexto sin
|
|
62
|
+
* que nadie lo compruebe, y el limite se choca a mitad de camino.
|
|
63
|
+
*
|
|
64
|
+
* 3. Resumir cuesta una llamada al modelo y pierde detalle. Vaciar las
|
|
65
|
+
* salidas de herramienta viejas no cuesta nada y casi siempre basta, asi
|
|
66
|
+
* que va primero.
|
|
67
|
+
*/
|
|
68
|
+
autoCompactIfNeeded(tokensActuales?: number): Promise<boolean>;
|
|
69
|
+
/**
|
|
70
|
+
* Verificacion del proyecto al cerrar un turno que escribio en disco.
|
|
71
|
+
*
|
|
72
|
+
* Devuelve el mensaje que hay que reinyectarle al modelo, o null si no hay
|
|
73
|
+
* nada que corregir (o si no toca verificar). El tope de vueltas evita el
|
|
74
|
+
* otro extremo: un agente peleandose solo con el compilador durante veinte
|
|
75
|
+
* iteraciones mientras el usuario mira.
|
|
76
|
+
*/
|
|
77
|
+
private verificarTrasEditar;
|
|
78
|
+
/**
|
|
79
|
+
* Comprueba si esta llamada cierra un bucle y, si lo cierra, decide con el
|
|
80
|
+
* usuario que hacer. Devuelve el texto que se le pasa al modelo: el resultado
|
|
81
|
+
* tal cual, o la explicacion del atasco.
|
|
82
|
+
*
|
|
83
|
+
* Se llama DESPUES de ejecutar, no antes, porque la firma incluye el
|
|
84
|
+
* resultado: sin el, un sondeo legitimo (releer un log que cambia) parece un
|
|
85
|
+
* bucle. La tercera llamada repetida ya se ha pagado; lo que se evita son la
|
|
86
|
+
* cuarta y las veintiseis siguientes.
|
|
87
|
+
*/
|
|
88
|
+
/** Apunta que este turno ha escrito en disco, para verificarlo al cerrarlo. */
|
|
89
|
+
private apuntarEscritura;
|
|
90
|
+
private vigilarBucle;
|
|
91
|
+
/**
|
|
92
|
+
* Ejecuta un turno completo y CUENTA como acabo.
|
|
93
|
+
*
|
|
94
|
+
* Antes devolvia void: el motivo del corte se imprimia por pantalla y ahi se
|
|
95
|
+
* moria. Quien llamaba no podia distinguir "terminado" de "cuota agotada al
|
|
96
|
+
* primer intento", que es justo lo que necesita el modo no interactivo para
|
|
97
|
+
* salir con un codigo honesto.
|
|
98
|
+
*/
|
|
99
|
+
runTask(userPrompt: string): Promise<ResultadoTarea>;
|
|
100
|
+
/**
|
|
101
|
+
* Guarda lo que el usuario acaba de aprobar con "no preguntar mas".
|
|
102
|
+
*
|
|
103
|
+
* Aqui se hacia command.split(" ")[0] y se guardaba solo el binario. Aprobar
|
|
104
|
+
* un "git status" dejaba el patron "git" escrito en .chocolatito/settings.json
|
|
105
|
+
* y, como el allow autoriza por prefijo, a partir de ese momento pasaban sin
|
|
106
|
+
* dialogo "git push --force" y "git reset --hard", en esa sesion y en todas
|
|
107
|
+
* las siguientes. Un solo si de mas y el proyecto se queda sin barrera.
|
|
108
|
+
*
|
|
109
|
+
* Ahora se guarda la firma del comando (binario mas subcomando) y, si el
|
|
110
|
+
* comando es de los destructivos, no se guarda nada: eso se pregunta siempre,
|
|
111
|
+
* y decirlo en voz alta es mejor que escribir un patron que no serviria.
|
|
112
|
+
*/
|
|
113
|
+
private recordarPermiso;
|
|
25
114
|
}
|