chocolatito-code 1.0.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/.mcp.json.ejemplo +19 -0
- package/COMPUTER-USE.md +484 -0
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/agent/context.d.ts +9 -0
- package/dist/agent/context.js +123 -0
- package/dist/agent/fileTracker.d.ts +16 -0
- package/dist/agent/fileTracker.js +53 -0
- package/dist/agent/loop.d.ts +25 -0
- package/dist/agent/loop.js +524 -0
- package/dist/agent/mentions.d.ts +11 -0
- package/dist/agent/mentions.js +43 -0
- package/dist/agent/retry.d.ts +17 -0
- package/dist/agent/retry.js +57 -0
- package/dist/agent/subagent.d.ts +2 -0
- package/dist/agent/subagent.js +116 -0
- package/dist/agent/syntaxValidator.d.ts +5 -0
- package/dist/agent/syntaxValidator.js +71 -0
- package/dist/agent/tracker.d.ts +19 -0
- package/dist/agent/tracker.js +38 -0
- package/dist/agent/undoManager.d.ts +37 -0
- package/dist/agent/undoManager.js +95 -0
- package/dist/config/constants.d.ts +77 -0
- package/dist/config/constants.js +146 -0
- package/dist/config/engine.d.ts +49 -0
- package/dist/config/engine.js +86 -0
- package/dist/config/env.d.ts +13 -0
- package/dist/config/env.js +103 -0
- package/dist/config/license.d.ts +39 -0
- package/dist/config/license.js +306 -0
- package/dist/config/permissions.d.ts +38 -0
- package/dist/config/permissions.js +239 -0
- package/dist/config/quota.d.ts +58 -0
- package/dist/config/quota.js +156 -0
- package/dist/hooks/manager.d.ts +60 -0
- package/dist/hooks/manager.js +193 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +470 -0
- package/dist/mcp/client.d.ts +55 -0
- package/dist/mcp/client.js +282 -0
- package/dist/mcp/manager.d.ts +26 -0
- package/dist/mcp/manager.js +167 -0
- package/dist/memory/manager.d.ts +9 -0
- package/dist/memory/manager.js +59 -0
- package/dist/prompts/systemPrompt.d.ts +5 -0
- package/dist/prompts/systemPrompt.js +177 -0
- package/dist/sessions/manager.d.ts +21 -0
- package/dist/sessions/manager.js +54 -0
- package/dist/skills/manager.d.ts +14 -0
- package/dist/skills/manager.js +86 -0
- package/dist/tools/browserCdp.d.ts +26 -0
- package/dist/tools/browserCdp.js +610 -0
- package/dist/tools/browserExtension.d.ts +58 -0
- package/dist/tools/browserExtension.js +479 -0
- package/dist/tools/browserLive.d.ts +12 -0
- package/dist/tools/browserLive.js +265 -0
- package/dist/tools/changeDir.d.ts +5 -0
- package/dist/tools/changeDir.js +63 -0
- package/dist/tools/computerUse.d.ts +58 -0
- package/dist/tools/computerUse.js +567 -0
- package/dist/tools/createImage.d.ts +1 -0
- package/dist/tools/createImage.js +34 -0
- package/dist/tools/definitions.d.ts +8 -0
- package/dist/tools/definitions.js +401 -0
- package/dist/tools/deleteFile.d.ts +1 -0
- package/dist/tools/deleteFile.js +17 -0
- package/dist/tools/editFile.d.ts +5 -0
- package/dist/tools/editFile.js +88 -0
- package/dist/tools/findFiles.d.ts +1 -0
- package/dist/tools/findFiles.js +33 -0
- package/dist/tools/getSystemInfo.d.ts +1 -0
- package/dist/tools/getSystemInfo.js +23 -0
- package/dist/tools/gitTools.d.ts +3 -0
- package/dist/tools/gitTools.js +47 -0
- package/dist/tools/grepSearch.d.ts +1 -0
- package/dist/tools/grepSearch.js +37 -0
- package/dist/tools/listDir.d.ts +1 -0
- package/dist/tools/listDir.js +33 -0
- package/dist/tools/moveCopyFile.d.ts +1 -0
- package/dist/tools/moveCopyFile.js +21 -0
- package/dist/tools/runCommand.d.ts +1 -0
- package/dist/tools/runCommand.js +61 -0
- package/dist/tools/runner.d.ts +5 -0
- package/dist/tools/runner.js +177 -0
- package/dist/tools/safety.d.ts +17 -0
- package/dist/tools/safety.js +33 -0
- package/dist/tools/todoTool.d.ts +8 -0
- package/dist/tools/todoTool.js +27 -0
- package/dist/tools/toolDefsComputer.d.ts +13 -0
- package/dist/tools/toolDefsComputer.js +205 -0
- package/dist/tools/truncator.d.ts +11 -0
- package/dist/tools/truncator.js +24 -0
- package/dist/tools/viewFile.d.ts +1 -0
- package/dist/tools/viewFile.js +45 -0
- package/dist/tools/visionBridge.d.ts +37 -0
- package/dist/tools/visionBridge.js +107 -0
- package/dist/tools/webFetch.d.ts +1 -0
- package/dist/tools/webFetch.js +25 -0
- package/dist/tools/webSearch.d.ts +1 -0
- package/dist/tools/webSearch.js +35 -0
- package/dist/tools/win/hostClient.d.ts +31 -0
- package/dist/tools/win/hostClient.js +158 -0
- package/dist/tools/win/hostScript.d.ts +1 -0
- package/dist/tools/win/hostScript.js +925 -0
- package/dist/tools/writeFile.d.ts +1 -0
- package/dist/tools/writeFile.js +13 -0
- package/dist/ui/banner.d.ts +10 -0
- package/dist/ui/banner.js +165 -0
- package/dist/ui/diffViewer.d.ts +1 -0
- package/dist/ui/diffViewer.js +60 -0
- package/dist/ui/interrupt.d.ts +17 -0
- package/dist/ui/interrupt.js +156 -0
- package/dist/ui/markdownStream.d.ts +23 -0
- package/dist/ui/markdownStream.js +0 -0
- package/dist/ui/modes.d.ts +28 -0
- package/dist/ui/modes.js +57 -0
- package/dist/ui/permissionPrompt.d.ts +35 -0
- package/dist/ui/permissionPrompt.js +271 -0
- package/dist/ui/prompt.d.ts +7 -0
- package/dist/ui/prompt.js +466 -0
- package/dist/ui/reasoningStream.d.ts +6 -0
- package/dist/ui/reasoningStream.js +38 -0
- package/dist/ui/renderer.d.ts +10 -0
- package/dist/ui/renderer.js +142 -0
- package/dist/ui/spinner.d.ts +24 -0
- package/dist/ui/spinner.js +111 -0
- package/dist/ui/theme.d.ts +36 -0
- package/dist/ui/theme.js +37 -0
- package/dist/ui/typeAhead.d.ts +31 -0
- package/dist/ui/typeAhead.js +87 -0
- package/extension/README.md +74 -0
- package/extension/background.js +575 -0
- package/extension/content.js +446 -0
- package/extension/manifest.json +36 -0
- package/extension/popup.html +22 -0
- package/extension/popup.js +12 -0
- package/package.json +66 -0
- package/skills/complex-refactor/SKILL.md +13 -0
- package/skills/deep-debugger/SKILL.md +14 -0
- package/skills/software-architect/SKILL.md +20 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { getAllTools } from "../tools/definitions.js";
|
|
4
|
+
import { executeToolCall } from "../tools/runner.js";
|
|
5
|
+
import { TokenTracker } from "./tracker.js";
|
|
6
|
+
import { renderToolStart, renderToolSuccess, renderToolError, renderDiff, renderFooter, renderError, } from "../ui/renderer.js";
|
|
7
|
+
import { CONTEXT_WINDOW, MAX_OUTPUT_TOKENS, COMPACT_THRESHOLD_TOKENS, } from "../config/constants.js";
|
|
8
|
+
import { withRetry } from "./retry.js";
|
|
9
|
+
import { canRunInParallel } from "../tools/safety.js";
|
|
10
|
+
import { createEngineClient, explicarErrorDelMotor } from "../config/engine.js";
|
|
11
|
+
import { MarkdownStream } from "../ui/markdownStream.js";
|
|
12
|
+
import { checkQuota, recordUsage, mensajeDeCorte, quotaBadge } from "../config/quota.js";
|
|
13
|
+
import { ReasoningStream } from "../ui/reasoningStream.js";
|
|
14
|
+
import { PermissionManager } from "../config/permissions.js";
|
|
15
|
+
import { askToolPermission } from "../ui/permissionPrompt.js";
|
|
16
|
+
import { DynamicSpinner } from "../ui/spinner.js";
|
|
17
|
+
import { globalUndoManager } from "./undoManager.js";
|
|
18
|
+
import { InterruptWatcher, isAbortError } from "../ui/interrupt.js";
|
|
19
|
+
import { HookManager } from "../hooks/manager.js";
|
|
20
|
+
export class AgentLoop {
|
|
21
|
+
client;
|
|
22
|
+
currentEspectro;
|
|
23
|
+
messages = [];
|
|
24
|
+
tracker;
|
|
25
|
+
currentCwd = process.cwd();
|
|
26
|
+
permissions;
|
|
27
|
+
hooks;
|
|
28
|
+
apiKey;
|
|
29
|
+
baseURL;
|
|
30
|
+
maxIterations = 30;
|
|
31
|
+
constructor(apiKey, baseURL, initialModel, systemPrompt) {
|
|
32
|
+
this.apiKey = apiKey;
|
|
33
|
+
this.baseURL = baseURL;
|
|
34
|
+
// Un unico sitio construye el cliente: asi la credencial de suscripcion y
|
|
35
|
+
// las cabeceras propias llegan igual desde las cuatro rutas.
|
|
36
|
+
this.client = createEngineClient(apiKey, "loop");
|
|
37
|
+
this.currentEspectro = initialModel;
|
|
38
|
+
this.tracker = new TokenTracker();
|
|
39
|
+
this.permissions = new PermissionManager("default", this.currentCwd);
|
|
40
|
+
this.hooks = new HookManager(this.currentCwd);
|
|
41
|
+
this.messages.push({
|
|
42
|
+
role: "system",
|
|
43
|
+
content: systemPrompt,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
setEspectroModel(model, updatedSystemPrompt) {
|
|
47
|
+
this.currentEspectro = model;
|
|
48
|
+
if (updatedSystemPrompt && this.messages.length > 0 && this.messages[0].role === "system") {
|
|
49
|
+
this.messages[0].content = updatedSystemPrompt;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
getEspectroModel() {
|
|
53
|
+
return this.currentEspectro;
|
|
54
|
+
}
|
|
55
|
+
clearHistory(systemPrompt) {
|
|
56
|
+
this.messages = [{ role: "system", content: systemPrompt }];
|
|
57
|
+
}
|
|
58
|
+
async compactHistory(systemPrompt) {
|
|
59
|
+
if (this.messages.length <= 4) {
|
|
60
|
+
return "El historial es breve, no requiere compactación.";
|
|
61
|
+
}
|
|
62
|
+
const spinner = new DynamicSpinner();
|
|
63
|
+
spinner.start("Compactando historial");
|
|
64
|
+
try {
|
|
65
|
+
const summaryPrompt = [
|
|
66
|
+
{
|
|
67
|
+
role: "system",
|
|
68
|
+
content: "Resume de forma ultra concisa los archivos modificados, decisiones tomadas y contexto clave de la conversación anterior para continuar trabajando.",
|
|
69
|
+
},
|
|
70
|
+
...this.messages.slice(1),
|
|
71
|
+
{
|
|
72
|
+
role: "user",
|
|
73
|
+
content: "Genera el resumen ejecutivo del contexto de trabajo hasta el momento.",
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
const res = await this.client.chat.completions.create({
|
|
77
|
+
model: this.currentEspectro.underlyingModel,
|
|
78
|
+
messages: summaryPrompt,
|
|
79
|
+
temperature: 0.1,
|
|
80
|
+
});
|
|
81
|
+
const summary = res.choices[0]?.message?.content || "Contexto previo compactado.";
|
|
82
|
+
spinner.stop();
|
|
83
|
+
this.messages = [
|
|
84
|
+
{ role: "system", content: systemPrompt },
|
|
85
|
+
{
|
|
86
|
+
role: "user",
|
|
87
|
+
content: `[RESUMEN DE CONTEXTO PREVIO COMPACTADO]:\n${summary}`,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
role: "assistant",
|
|
91
|
+
content: "Entendido. He retenido el contexto clave del proyecto y estoy listo para continuar.",
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
return `✔ Memoria optimizada y compactada exitosamente (${summary.length} caracteres de contexto retenidos).`;
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
spinner.stop();
|
|
98
|
+
return `Fallo al compactar historial: ${err?.message || String(err)}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
lastTurnTokens = 0;
|
|
102
|
+
async autoCompactIfNeeded() {
|
|
103
|
+
// La ventana real es de 1.000.000 de tokens. Antes se compactaba a los 16
|
|
104
|
+
// mensajes o 38k tokens, es decir al 4% de su capacidad: el agente perdia
|
|
105
|
+
// el hilo constantemente y repetia trabajo ya hecho. Ahora solo se comprime
|
|
106
|
+
// cuando de verdad se acerca el limite.
|
|
107
|
+
if (this.lastTurnTokens < COMPACT_THRESHOLD_TOKENS) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const activeSystemPrompt = this.messages[0]?.role === "system" ? this.messages[0].content : "";
|
|
112
|
+
const toSummarize = this.messages.slice(1, -4);
|
|
113
|
+
if (toSummarize.length < 4)
|
|
114
|
+
return false;
|
|
115
|
+
const spinner = new DynamicSpinner();
|
|
116
|
+
spinner.start("Auto-optimizando memoria del contexto");
|
|
117
|
+
const summaryPrompt = [
|
|
118
|
+
{
|
|
119
|
+
role: "system",
|
|
120
|
+
content: "Resume de forma ultra concisa los archivos modificados, decisiones tomadas, arquitectura y estado actual del proyecto para continuar trabajando sin perder el hilo.",
|
|
121
|
+
},
|
|
122
|
+
...toSummarize,
|
|
123
|
+
{
|
|
124
|
+
role: "user",
|
|
125
|
+
content: "Genera el resumen ejecutivo del contexto de trabajo previo.",
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
const res = await this.client.chat.completions.create({
|
|
129
|
+
model: this.currentEspectro.underlyingModel,
|
|
130
|
+
messages: summaryPrompt,
|
|
131
|
+
temperature: 0.1,
|
|
132
|
+
});
|
|
133
|
+
const summary = res.choices[0]?.message?.content || "Contexto previo optimizado.";
|
|
134
|
+
spinner.stop();
|
|
135
|
+
const recentMessages = this.messages.slice(-4);
|
|
136
|
+
this.messages = [
|
|
137
|
+
{ role: "system", content: activeSystemPrompt },
|
|
138
|
+
{
|
|
139
|
+
role: "user",
|
|
140
|
+
content: `[RESUMEN DE AUTO-COMPACTACIÓN PREVIA]:\n${summary}`,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
role: "assistant",
|
|
144
|
+
content: "Entendido. He retenido el contexto clave del proyecto y liberado memoria.",
|
|
145
|
+
},
|
|
146
|
+
...recentMessages,
|
|
147
|
+
];
|
|
148
|
+
console.log(chalk.hex("#D97757")("\n⚡ [Auto-Compact]: ") +
|
|
149
|
+
chalk.gray(`Contexto optimizado automáticamente (${this.messages.length} mensajes limpios) para mantener alta velocidad y mínimo consumo de API.\n`));
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async runTask(userPrompt) {
|
|
157
|
+
// Cuota ANTES de gastar. Si no queda margen, se dice y no se llama al motor:
|
|
158
|
+
// avisar despues de haber gastado no sirve de nada.
|
|
159
|
+
const cuota = checkQuota();
|
|
160
|
+
if (!cuota.allowed) {
|
|
161
|
+
console.log(chalk.yellow(`\n⏸ ${mensajeDeCorte(cuota)}`) +
|
|
162
|
+
chalk.gray("\n Tu trabajo y tu contexto siguen aqui: al liberarse, continuas donde lo dejaste.\n"));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// Check and execute auto-compaction if context threshold is exceeded
|
|
166
|
+
await this.autoCompactIfNeeded();
|
|
167
|
+
const startTime = Date.now();
|
|
168
|
+
this.messages.push({
|
|
169
|
+
role: "user",
|
|
170
|
+
content: userPrompt,
|
|
171
|
+
});
|
|
172
|
+
let iterations = 0;
|
|
173
|
+
let turnPromptTokens = 0;
|
|
174
|
+
let turnCachedTokens = 0;
|
|
175
|
+
let turnCompletionTokens = 0;
|
|
176
|
+
while (iterations < this.maxIterations) {
|
|
177
|
+
iterations++;
|
|
178
|
+
// Near iteration limit notice
|
|
179
|
+
if (iterations === this.maxIterations - 2) {
|
|
180
|
+
this.messages.push({
|
|
181
|
+
role: "system",
|
|
182
|
+
content: "Aviso del sistema: Te quedan 2 iteraciones. Cierra lo que estés haciendo y resume el estado actual del trabajo.",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const spinner = new DynamicSpinner();
|
|
186
|
+
spinner.tokenCount = turnCompletionTokens;
|
|
187
|
+
spinner.start();
|
|
188
|
+
// Escucha Esc mientras dura la peticion. Se para siempre en el finally.
|
|
189
|
+
const watcher = new InterruptWatcher();
|
|
190
|
+
const signal = watcher.start();
|
|
191
|
+
let stream;
|
|
192
|
+
try {
|
|
193
|
+
// Un 429 o un 500 pasajero no deberia costar el turno entero.
|
|
194
|
+
stream = await withRetry(() => this.client.chat.completions.create({
|
|
195
|
+
model: this.currentEspectro.underlyingModel,
|
|
196
|
+
messages: this.messages,
|
|
197
|
+
tools: getAllTools(),
|
|
198
|
+
tool_choice: "auto",
|
|
199
|
+
temperature: this.currentEspectro.temperature,
|
|
200
|
+
max_tokens: MAX_OUTPUT_TOKENS,
|
|
201
|
+
stream: true,
|
|
202
|
+
stream_options: { include_usage: true },
|
|
203
|
+
}, { signal }), {
|
|
204
|
+
onRetry: (attempt, delay, reason) => {
|
|
205
|
+
spinner.stop();
|
|
206
|
+
console.log(chalk.yellow(`\n⟳ El motor respondio ${reason}. Reintento ${attempt} en ${(delay / 1000).toFixed(1)}s...`));
|
|
207
|
+
spinner.start();
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
spinner.stop();
|
|
213
|
+
watcher.stop();
|
|
214
|
+
if (isAbortError(err) || watcher.interrupted) {
|
|
215
|
+
console.log(chalk.yellow("\n⏸ Interrumpido. Dime como seguir."));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// Un 402 (cuota agotada) o un 401 (licencia caida) no son "fallos del
|
|
219
|
+
// motor" y ademas no se reintentan nunca: decirlo como tal confunde y
|
|
220
|
+
// hace que el usuario piense que el producto esta roto.
|
|
221
|
+
const claro = explicarErrorDelMotor(err);
|
|
222
|
+
if (claro) {
|
|
223
|
+
console.log(chalk.yellow(`
|
|
224
|
+
⏸ ${claro}
|
|
225
|
+
`));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
renderError(`Error en el motor tras varios reintentos: ${err?.message || String(err)}`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
let content = "";
|
|
232
|
+
let reasoning = "";
|
|
233
|
+
const rawToolCalls = [];
|
|
234
|
+
let isFirstChunk = true;
|
|
235
|
+
const reasoningStream = new ReasoningStream();
|
|
236
|
+
const markdown = new MarkdownStream();
|
|
237
|
+
try {
|
|
238
|
+
for await (const chunk of stream) {
|
|
239
|
+
if (isFirstChunk) {
|
|
240
|
+
spinner.stop();
|
|
241
|
+
isFirstChunk = false;
|
|
242
|
+
}
|
|
243
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
244
|
+
// Handle real-time reasoning stream
|
|
245
|
+
if (delta?.reasoning_content) {
|
|
246
|
+
const rText = delta.reasoning_content;
|
|
247
|
+
reasoning += rText;
|
|
248
|
+
reasoningStream.push(rText);
|
|
249
|
+
}
|
|
250
|
+
// Handle real-time text streaming
|
|
251
|
+
if (delta?.content) {
|
|
252
|
+
reasoningStream.flush();
|
|
253
|
+
content += delta.content;
|
|
254
|
+
markdown.push(delta.content);
|
|
255
|
+
}
|
|
256
|
+
// Accumulate fragmented tool calls by index tc.index
|
|
257
|
+
if (delta?.tool_calls) {
|
|
258
|
+
for (const tc of delta.tool_calls) {
|
|
259
|
+
const idx = tc.index ?? 0;
|
|
260
|
+
if (!rawToolCalls[idx]) {
|
|
261
|
+
rawToolCalls[idx] = {
|
|
262
|
+
id: tc.id || "",
|
|
263
|
+
type: "function",
|
|
264
|
+
function: { name: "", arguments: "" },
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (tc.id)
|
|
268
|
+
rawToolCalls[idx].id = tc.id;
|
|
269
|
+
if (tc.function?.name)
|
|
270
|
+
rawToolCalls[idx].function.name += tc.function.name;
|
|
271
|
+
if (tc.function?.arguments)
|
|
272
|
+
rawToolCalls[idx].function.arguments += tc.function.arguments;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Track streaming usage metrics
|
|
276
|
+
if (chunk.usage) {
|
|
277
|
+
this.tracker.addUsage(chunk.usage);
|
|
278
|
+
turnPromptTokens += chunk.usage.prompt_tokens || 0;
|
|
279
|
+
turnCompletionTokens += chunk.usage.completion_tokens || 0;
|
|
280
|
+
turnCachedTokens += chunk.usage.prompt_tokens_details?.cached_tokens || 0;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch (streamErr) {
|
|
285
|
+
spinner.stop();
|
|
286
|
+
watcher.stop();
|
|
287
|
+
reasoningStream.flush();
|
|
288
|
+
markdown.flush();
|
|
289
|
+
if (isAbortError(streamErr) || watcher.interrupted) {
|
|
290
|
+
// Se conserva lo que ya habia escrito: es trabajo util aunque este a
|
|
291
|
+
// medias, y perderlo obliga al usuario a repetir la peticion.
|
|
292
|
+
if (content.trim()) {
|
|
293
|
+
this.messages.push({ role: "assistant", content });
|
|
294
|
+
}
|
|
295
|
+
console.log(chalk.yellow("\n⏸ Interrumpido. Dime como seguir."));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const claroStream = explicarErrorDelMotor(streamErr);
|
|
299
|
+
if (claroStream) {
|
|
300
|
+
console.log(chalk.yellow(`
|
|
301
|
+
⏸ ${claroStream}
|
|
302
|
+
`));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
renderError(`Error en streaming: ${streamErr?.message || String(streamErr)}`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
spinner.stop();
|
|
309
|
+
reasoningStream.flush();
|
|
310
|
+
markdown.flush();
|
|
311
|
+
if (watcher.interrupted) {
|
|
312
|
+
watcher.stop();
|
|
313
|
+
if (content.trim())
|
|
314
|
+
this.messages.push({ role: "assistant", content });
|
|
315
|
+
console.log(chalk.yellow("\n⏸ Interrumpido antes de ejecutar las herramientas. Dime como seguir."));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
// Filter valid accumulated tool calls
|
|
319
|
+
const toolCalls = rawToolCalls.filter((tc) => tc && tc.function && tc.function.name);
|
|
320
|
+
if (toolCalls.length > 0) {
|
|
321
|
+
this.messages.push({
|
|
322
|
+
role: "assistant",
|
|
323
|
+
content: content || null,
|
|
324
|
+
tool_calls: toolCalls,
|
|
325
|
+
});
|
|
326
|
+
const toolResults = [];
|
|
327
|
+
// Las llamadas independientes de solo lectura se lanzan a la vez. Cuando
|
|
328
|
+
// el modelo pide cuatro lecturas de archivo, hacerlas en serie multiplica
|
|
329
|
+
// la espera por cuatro sin ganar nada.
|
|
330
|
+
const names = toolCalls.map((tc) => tc.function.name);
|
|
331
|
+
const parallel = canRunInParallel(names) &&
|
|
332
|
+
!toolCalls.some((tc) => {
|
|
333
|
+
let a = {};
|
|
334
|
+
try {
|
|
335
|
+
a = JSON.parse(tc.function.arguments);
|
|
336
|
+
}
|
|
337
|
+
catch { }
|
|
338
|
+
return this.permissions.requiresConfirmation(tc.function.name, a);
|
|
339
|
+
});
|
|
340
|
+
if (parallel) {
|
|
341
|
+
for (const tc of toolCalls) {
|
|
342
|
+
let a = {};
|
|
343
|
+
try {
|
|
344
|
+
a = JSON.parse(tc.function.arguments);
|
|
345
|
+
}
|
|
346
|
+
catch { }
|
|
347
|
+
renderToolStart(tc.function.name, getToolSummary(tc.function.name, a));
|
|
348
|
+
}
|
|
349
|
+
const settled = await Promise.all(toolCalls.map(async (tc) => {
|
|
350
|
+
let a = {};
|
|
351
|
+
try {
|
|
352
|
+
a = JSON.parse(tc.function.arguments);
|
|
353
|
+
}
|
|
354
|
+
catch { }
|
|
355
|
+
const r = await executeToolCall(tc.function.name, a, this.currentCwd, this.apiKey, this.currentEspectro.underlyingModel);
|
|
356
|
+
return { tc, result: r.result };
|
|
357
|
+
}));
|
|
358
|
+
for (const { tc, result } of settled) {
|
|
359
|
+
renderToolSuccess(tc.function.name, result);
|
|
360
|
+
toolResults.push({ role: "tool", tool_call_id: tc.id, content: result });
|
|
361
|
+
}
|
|
362
|
+
toolResults.forEach((r) => this.messages.push(r));
|
|
363
|
+
watcher.stop();
|
|
364
|
+
if (watcher.interrupted) {
|
|
365
|
+
console.log(chalk.yellow("\n⏸ Interrumpido. Dime como seguir."));
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
for (const tc of toolCalls) {
|
|
371
|
+
// Entre herramienta y herramienta se mira si el usuario pidio parar.
|
|
372
|
+
// Asi una cadena larga de acciones se puede cortar sin matar la sesion.
|
|
373
|
+
if (watcher.interrupted) {
|
|
374
|
+
toolResults.push({
|
|
375
|
+
role: "tool",
|
|
376
|
+
tool_call_id: tc.id,
|
|
377
|
+
content: "No ejecutada: el usuario interrumpió la tarea.",
|
|
378
|
+
});
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const fnName = tc.function.name;
|
|
382
|
+
let fnArgs = {};
|
|
383
|
+
try {
|
|
384
|
+
fnArgs = JSON.parse(tc.function.arguments);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
fnArgs = {};
|
|
388
|
+
}
|
|
389
|
+
// PreToolUse: el usuario puede vetar, autorizar o reescribir los
|
|
390
|
+
// argumentos ANTES de que se ejecute nada. Va antes del dialogo de
|
|
391
|
+
// permiso porque un veto no deberia ni llegar a preguntarse.
|
|
392
|
+
const pre = await this.hooks.run("PreToolUse", { tool_name: fnName, tool_input: fnArgs }, fnName);
|
|
393
|
+
if (pre.permissionDecision === "deny") {
|
|
394
|
+
renderToolStart(fnName, getToolSummary(fnName, fnArgs));
|
|
395
|
+
renderToolError(fnName, pre.permissionDecisionReason || "Bloqueado por un hook.");
|
|
396
|
+
toolResults.push({
|
|
397
|
+
role: "tool",
|
|
398
|
+
tool_call_id: tc.id,
|
|
399
|
+
content: `Un hook del proyecto bloqueo esta accion: ${pre.permissionDecisionReason || "sin motivo"}. No la reintentes igual; busca otra via o preguntale al usuario.`,
|
|
400
|
+
});
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (pre.updatedInput) {
|
|
404
|
+
fnArgs = { ...fnArgs, ...pre.updatedInput };
|
|
405
|
+
}
|
|
406
|
+
if (pre.systemMessage) {
|
|
407
|
+
console.log(chalk.yellow(`\n⚑ ${pre.systemMessage}`));
|
|
408
|
+
}
|
|
409
|
+
// Modo plan: bloqueo duro, no un dialogo. Lo que el modo promete es
|
|
410
|
+
// solo lectura, asi que aqui no hay nada que preguntar.
|
|
411
|
+
const bloqueoPlan = this.permissions.isBlockedInPlanMode(fnName, fnArgs);
|
|
412
|
+
if (bloqueoPlan) {
|
|
413
|
+
renderToolStart(fnName, getToolSummary(fnName, fnArgs));
|
|
414
|
+
renderToolError(fnName, bloqueoPlan);
|
|
415
|
+
toolResults.push({
|
|
416
|
+
role: "tool",
|
|
417
|
+
tool_call_id: tc.id,
|
|
418
|
+
content: `${bloqueoPlan} Termina el plan y pide al usuario que salga del modo plan para ejecutarlo.`,
|
|
419
|
+
});
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
// Permission gate
|
|
423
|
+
const saltaPermiso = pre.permissionDecision === "allow";
|
|
424
|
+
if (!saltaPermiso && this.permissions.requiresConfirmation(fnName, fnArgs)) {
|
|
425
|
+
watcher.stop();
|
|
426
|
+
const permResult = await askToolPermission(fnName, fnArgs, this.currentCwd);
|
|
427
|
+
if (!permResult.approved) {
|
|
428
|
+
const rejectReason = `El usuario canceló la ejecución de este comando con el mensaje: "${permResult.userFeedback || 'Cancelado'}". Ajusta tu enfoque según sus indicaciones y no reintentes la misma acción sin cambios.`;
|
|
429
|
+
renderToolStart(fnName, getToolSummary(fnName, fnArgs));
|
|
430
|
+
renderToolError(fnName, permResult.userFeedback || "Acción rechazada por el usuario.");
|
|
431
|
+
toolResults.push({
|
|
432
|
+
role: "tool",
|
|
433
|
+
tool_call_id: tc.id,
|
|
434
|
+
content: rejectReason,
|
|
435
|
+
});
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (permResult.rememberPattern) {
|
|
439
|
+
const pat = fnArgs.command ? fnArgs.command.split(" ")[0] : fnArgs.path || fnName;
|
|
440
|
+
this.permissions.saveAllowPattern(pat);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const summary = getToolSummary(fnName, fnArgs);
|
|
444
|
+
renderToolStart(fnName, summary);
|
|
445
|
+
const { result, diffText, newCwd } = await executeToolCall(fnName, fnArgs, this.currentCwd, this.apiKey, this.currentEspectro.underlyingModel);
|
|
446
|
+
if (newCwd) {
|
|
447
|
+
this.currentCwd = newCwd;
|
|
448
|
+
}
|
|
449
|
+
if (diffText && fnArgs.path) {
|
|
450
|
+
renderDiff(fnArgs.path, diffText);
|
|
451
|
+
}
|
|
452
|
+
renderToolSuccess(fnName, result);
|
|
453
|
+
// PostToolUse: sirve para validar lo que acaba de pasar (linters,
|
|
454
|
+
// tests, politicas) y devolverle al modelo un aviso accionable.
|
|
455
|
+
const post = await this.hooks.run("PostToolUse", { tool_name: fnName, tool_input: fnArgs, tool_response: result.slice(0, 4000) }, fnName);
|
|
456
|
+
toolResults.push({
|
|
457
|
+
role: "tool",
|
|
458
|
+
tool_call_id: tc.id,
|
|
459
|
+
content: post.additionalContext ? `${result}\n\n
|
|
460
|
+
[Hook PostToolUse]: ${post.additionalContext}` : result,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
// Push all tool results in a single turn
|
|
464
|
+
toolResults.forEach((r) => this.messages.push(r));
|
|
465
|
+
watcher.stop();
|
|
466
|
+
if (watcher.interrupted) {
|
|
467
|
+
console.log(chalk.yellow("\n⏸ Interrumpido. Dime como seguir."));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
watcher.stop();
|
|
473
|
+
// If text content was printed, ensure newline and record assistant message
|
|
474
|
+
if (content.trim()) {
|
|
475
|
+
process.stdout.write("\n\n");
|
|
476
|
+
this.messages.push({
|
|
477
|
+
role: "assistant",
|
|
478
|
+
content,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
const durationMs = Date.now() - startTime;
|
|
484
|
+
const { formattedCost, totalCost } = this.tracker.calculateCost(turnPromptTokens, turnCachedTokens, turnCompletionTokens, this.currentEspectro.underlyingModel);
|
|
485
|
+
const approxTotalTokens = turnPromptTokens + turnCompletionTokens;
|
|
486
|
+
this.lastTurnTokens = approxTotalTokens;
|
|
487
|
+
// El porcentaje libre se calcula sobre los tokens que ocupa el historial
|
|
488
|
+
// (el prompt del ultimo turno), no sobre prompt+salida, que contaba doble.
|
|
489
|
+
const used = turnPromptTokens || approxTotalTokens;
|
|
490
|
+
const contextPercent = Math.max(1, 100 - Math.round((used / CONTEXT_WINDOW) * 100));
|
|
491
|
+
globalUndoManager.commitTurn(userPrompt);
|
|
492
|
+
// Se apunta lo gastado en este turno para las ventanas deslizantes.
|
|
493
|
+
recordUsage(totalCost);
|
|
494
|
+
const tras = checkQuota();
|
|
495
|
+
renderFooter(formattedCost, turnCompletionTokens, turnCachedTokens, this.currentEspectro.name, durationMs, contextPercent, quotaBadge(tras));
|
|
496
|
+
// Aviso al acercarse, no solo al chocar: quedarse a medias sin previo aviso
|
|
497
|
+
// es lo que hace que la gente cancele.
|
|
498
|
+
if (tras.allowed && tras.spentWeekUsd >= tras.config.weeklyBudgetUsd * 0.8) {
|
|
499
|
+
const queda = Math.max(0, tras.config.weeklyBudgetUsd - tras.spentWeekUsd);
|
|
500
|
+
console.log(chalk.yellow(` ⚠ Te queda poca cuota semanal (${((queda / tras.config.weeklyBudgetUsd) * 100).toFixed(0)}%).\n`));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function getToolSummary(name, args) {
|
|
505
|
+
if (args.tasks && Array.isArray(args.tasks))
|
|
506
|
+
return `${args.tasks.length} tareas`;
|
|
507
|
+
if (args.skillName)
|
|
508
|
+
return args.skillName;
|
|
509
|
+
if (args.topic)
|
|
510
|
+
return args.topic;
|
|
511
|
+
if (args.type)
|
|
512
|
+
return `${args.type}: "${args.prompt?.slice(0, 30)}..."`;
|
|
513
|
+
if (args.path)
|
|
514
|
+
return args.path + (args.startLine ? ` (L${args.startLine}-${args.endLine || ""})` : "");
|
|
515
|
+
if (args.command)
|
|
516
|
+
return args.command;
|
|
517
|
+
if (args.query)
|
|
518
|
+
return `"${args.query}"`;
|
|
519
|
+
if (args.dirPath)
|
|
520
|
+
return args.dirPath;
|
|
521
|
+
if (args.action)
|
|
522
|
+
return args.action + (args.url ? ` (${args.url})` : "");
|
|
523
|
+
return "";
|
|
524
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface MentionedFile {
|
|
2
|
+
mention: string;
|
|
3
|
+
relativePath: string;
|
|
4
|
+
content: string;
|
|
5
|
+
size: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function parseFileMentions(userInput: string, cwd?: string): {
|
|
8
|
+
cleanPrompt: string;
|
|
9
|
+
attachedFiles: MentionedFile[];
|
|
10
|
+
};
|
|
11
|
+
export declare function buildPromptWithMentions(userInput: string, attachedFiles: MentionedFile[]): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
export function parseFileMentions(userInput, cwd = process.cwd()) {
|
|
5
|
+
const mentionRegex = /@([a-zA-Z0-9_\-\.\/\\]+)/g;
|
|
6
|
+
const attachedFiles = [];
|
|
7
|
+
let match;
|
|
8
|
+
while ((match = mentionRegex.exec(userInput)) !== null) {
|
|
9
|
+
const rawPath = match[1];
|
|
10
|
+
const resolved = path.isAbsolute(rawPath) ? rawPath : path.resolve(cwd, rawPath);
|
|
11
|
+
if (fs.existsSync(resolved)) {
|
|
12
|
+
try {
|
|
13
|
+
const stat = fs.statSync(resolved);
|
|
14
|
+
if (stat.isFile()) {
|
|
15
|
+
const content = fs.readFileSync(resolved, "utf-8");
|
|
16
|
+
attachedFiles.push({
|
|
17
|
+
mention: match[0],
|
|
18
|
+
relativePath: path.relative(cwd, resolved) || rawPath,
|
|
19
|
+
content: content.slice(0, 15_000),
|
|
20
|
+
size: stat.size,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch { }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (attachedFiles.length > 0) {
|
|
28
|
+
console.log(chalk.hex("#A855F7")(` 📎 Adjuntando automáticamente ${attachedFiles.length} archivo(s) referenciado(s) con @:\n` +
|
|
29
|
+
attachedFiles.map((f) => ` • ${chalk.white(f.relativePath)} (${(f.size / 1024).toFixed(1)} KB)`).join("\n")));
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
cleanPrompt: userInput,
|
|
33
|
+
attachedFiles,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function buildPromptWithMentions(userInput, attachedFiles) {
|
|
37
|
+
if (attachedFiles.length === 0)
|
|
38
|
+
return userInput;
|
|
39
|
+
const fileBlocks = attachedFiles
|
|
40
|
+
.map((f) => `\n\n--- CONTENIDO ADJUNTO DE @${f.relativePath} ---\n${f.content}\n--- FIN DE @${f.relativePath} ---`)
|
|
41
|
+
.join("\n");
|
|
42
|
+
return `${userInput}\n${fileBlocks}`;
|
|
43
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reintentos con espera exponencial para las llamadas al motor.
|
|
3
|
+
*
|
|
4
|
+
* Antes, cualquier hipo de la API (un 429 por limite de ritmo, un 500, una
|
|
5
|
+
* conexion cortada) abortaba el turno entero con "Error en el motor" y el
|
|
6
|
+
* usuario perdia el trabajo en curso. Un fallo transitorio no deberia costar
|
|
7
|
+
* una tarea.
|
|
8
|
+
*/
|
|
9
|
+
export interface RetryOptions {
|
|
10
|
+
attempts?: number;
|
|
11
|
+
baseDelayMs?: number;
|
|
12
|
+
maxDelayMs?: number;
|
|
13
|
+
onRetry?: (attempt: number, delayMs: number, reason: string) => void;
|
|
14
|
+
}
|
|
15
|
+
/** Errores que merece la pena reintentar: transitorios, no de programacion. */
|
|
16
|
+
export declare function isRetryable(err: any): boolean;
|
|
17
|
+
export declare function withRetry<T>(fn: () => Promise<T>, opts?: RetryOptions): Promise<T>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reintentos con espera exponencial para las llamadas al motor.
|
|
3
|
+
*
|
|
4
|
+
* Antes, cualquier hipo de la API (un 429 por limite de ritmo, un 500, una
|
|
5
|
+
* conexion cortada) abortaba el turno entero con "Error en el motor" y el
|
|
6
|
+
* usuario perdia el trabajo en curso. Un fallo transitorio no deberia costar
|
|
7
|
+
* una tarea.
|
|
8
|
+
*/
|
|
9
|
+
/** Errores que merece la pena reintentar: transitorios, no de programacion. */
|
|
10
|
+
export function isRetryable(err) {
|
|
11
|
+
const status = err?.status ?? err?.response?.status;
|
|
12
|
+
if (typeof status === "number") {
|
|
13
|
+
// 429 limite de ritmo; 5xx problemas del servidor. Un 400/401/403 es culpa
|
|
14
|
+
// nuestra y reintentar solo hace perder tiempo.
|
|
15
|
+
return status === 408 || status === 409 || status === 429 || status >= 500;
|
|
16
|
+
}
|
|
17
|
+
const code = String(err?.code || "");
|
|
18
|
+
if (["ECONNRESET", "ETIMEDOUT", "ECONNREFUSED", "EPIPE", "ENOTFOUND", "EAI_AGAIN"].includes(code)) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
const msg = String(err?.message || "").toLowerCase();
|
|
22
|
+
return (msg.includes("timeout") ||
|
|
23
|
+
msg.includes("socket hang up") ||
|
|
24
|
+
msg.includes("network") ||
|
|
25
|
+
msg.includes("fetch failed") ||
|
|
26
|
+
msg.includes("terminated"));
|
|
27
|
+
}
|
|
28
|
+
function describe(err) {
|
|
29
|
+
const status = err?.status ?? err?.response?.status;
|
|
30
|
+
if (status)
|
|
31
|
+
return `HTTP ${status}`;
|
|
32
|
+
if (err?.code)
|
|
33
|
+
return String(err.code);
|
|
34
|
+
return String(err?.message || err).slice(0, 120);
|
|
35
|
+
}
|
|
36
|
+
export async function withRetry(fn, opts = {}) {
|
|
37
|
+
const attempts = opts.attempts ?? 4;
|
|
38
|
+
const base = opts.baseDelayMs ?? 800;
|
|
39
|
+
const max = opts.maxDelayMs ?? 15_000;
|
|
40
|
+
let lastErr;
|
|
41
|
+
for (let i = 0; i < attempts; i++) {
|
|
42
|
+
try {
|
|
43
|
+
return await fn();
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
lastErr = err;
|
|
47
|
+
const isLast = i === attempts - 1;
|
|
48
|
+
if (isLast || !isRetryable(err))
|
|
49
|
+
throw err;
|
|
50
|
+
// Espera exponencial con jitter, para no sincronizar reintentos.
|
|
51
|
+
const delay = Math.min(max, base * Math.pow(2, i)) * (0.7 + Math.random() * 0.6);
|
|
52
|
+
opts.onRetry?.(i + 1, Math.round(delay), describe(err));
|
|
53
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
throw lastErr;
|
|
57
|
+
}
|