vipcode 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.en.md +56 -0
- package/README.md +382 -2
- package/dist/agent.js +502 -0
- package/dist/agents.js +171 -0
- package/dist/app.js +1013 -0
- package/dist/auth.js +108 -0
- package/dist/catalog.js +487 -0
- package/dist/commands.js +119 -0
- package/dist/config.js +225 -0
- package/dist/debug.js +32 -0
- package/dist/discovery.js +277 -0
- package/dist/frontmatter.js +45 -0
- package/dist/images.js +81 -0
- package/dist/index.js +574 -0
- package/dist/login.js +157 -0
- package/dist/lsp.js +480 -0
- package/dist/markdown.js +259 -0
- package/dist/match.js +8 -0
- package/dist/mcp.js +225 -0
- package/dist/meta.js +7 -0
- package/dist/paths.js +27 -0
- package/dist/permissions.js +120 -0
- package/dist/prompt.js +123 -0
- package/dist/ranking.js +230 -0
- package/dist/router.js +726 -0
- package/dist/session.js +219 -0
- package/dist/skills.js +373 -0
- package/dist/snapshot.js +199 -0
- package/dist/tools.js +1247 -0
- package/dist/tui/app.js +1015 -0
- package/dist/tui/dialogs.js +196 -0
- package/dist/tui/editor.js +120 -0
- package/dist/ui.js +181 -0
- package/dist/update.js +235 -0
- package/package.json +64 -4
package/dist/agent.js
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { subagents, toolEnabled } from "./agents.js";
|
|
2
|
+
import { systemPrompt } from "./prompt.js";
|
|
3
|
+
import { estimateTokens } from "./router.js";
|
|
4
|
+
import { saveSession } from "./session.js";
|
|
5
|
+
import { skillTool } from "./skills.js";
|
|
6
|
+
import { CORE_TOOLS, beginTurn, describeCall, expandMentions, runTool, taskTool, toolDefinitions, truncate, undoLastTurn, } from "./tools.js";
|
|
7
|
+
const COMPACT_PROMPT = `Summarize this conversation so another engineer can continue the work without the history.
|
|
8
|
+
Include: the user's goal, decisions taken, files read or modified (with paths), the current state and pending steps.
|
|
9
|
+
Be concrete and complete but brief. Write it in the language of the conversation.`;
|
|
10
|
+
export class Agent {
|
|
11
|
+
deps;
|
|
12
|
+
session;
|
|
13
|
+
agent;
|
|
14
|
+
events = {};
|
|
15
|
+
// Mensajes que escribes mientras trabaja: se le entregan en cuanto acaba el paso actual
|
|
16
|
+
queue = [];
|
|
17
|
+
// Ficheros leídos y cuándo (para no editar a ciegas)
|
|
18
|
+
readTimes = new Map();
|
|
19
|
+
lastSlot = "";
|
|
20
|
+
// Tokens de la última petición (los que dice el proveedor o los estimados)
|
|
21
|
+
lastTokens = 0;
|
|
22
|
+
running = false;
|
|
23
|
+
// Aviso para el modelo que se añade al siguiente mensaje (por ejemplo tras /undo)
|
|
24
|
+
pendingNote = "";
|
|
25
|
+
constructor(deps, session, agentName = "build") {
|
|
26
|
+
this.deps = deps;
|
|
27
|
+
this.session = session;
|
|
28
|
+
this.agent = deps.agents.find((agent) => agent.name === agentName) || deps.agents.find((agent) => agent.name === "build");
|
|
29
|
+
}
|
|
30
|
+
get mode() {
|
|
31
|
+
if (this.agent.readOnly) {
|
|
32
|
+
return "plan";
|
|
33
|
+
}
|
|
34
|
+
return "build";
|
|
35
|
+
}
|
|
36
|
+
setAgent(name) {
|
|
37
|
+
const found = this.deps.agents.find((agent) => agent.name === name);
|
|
38
|
+
if (!found) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
this.agent = found;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
updateDeps(changes) {
|
|
45
|
+
Object.assign(this.deps, changes);
|
|
46
|
+
const current = this.deps.agents.find((agent) => agent.name === this.agent.name);
|
|
47
|
+
if (current) {
|
|
48
|
+
this.agent = current;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
setSession(session) {
|
|
52
|
+
this.session = session;
|
|
53
|
+
this.readTimes.clear();
|
|
54
|
+
this.pendingNote = "";
|
|
55
|
+
this.lastTokens = 0;
|
|
56
|
+
}
|
|
57
|
+
// Herramientas de un agente: las propias, task (si hay subagentes) y las de MCP, con sus filtros
|
|
58
|
+
toolsFor(agent, depth) {
|
|
59
|
+
let tools = [...CORE_TOOLS];
|
|
60
|
+
const subs = subagents(this.deps.agents).filter((sub) => sub.name !== agent.name);
|
|
61
|
+
if (depth === 0 && subs.length > 0) {
|
|
62
|
+
tools.push(taskTool(subs));
|
|
63
|
+
}
|
|
64
|
+
if (this.deps.skills.length > 0) {
|
|
65
|
+
tools.push(skillTool(this.deps.skills));
|
|
66
|
+
}
|
|
67
|
+
if (this.deps.mcp) {
|
|
68
|
+
tools.push(...this.deps.mcp.tools());
|
|
69
|
+
}
|
|
70
|
+
if (!this.deps.lsp || !this.deps.lsp.enabled()) {
|
|
71
|
+
tools = tools.filter((tool) => tool.name !== "diagnostics");
|
|
72
|
+
}
|
|
73
|
+
if (depth > 0) {
|
|
74
|
+
tools = tools.filter((tool) => tool.name !== "todowrite" && tool.name !== "todoread");
|
|
75
|
+
}
|
|
76
|
+
tools = tools.filter((tool) => toolEnabled(agent, tool.name));
|
|
77
|
+
if (agent.readOnly) {
|
|
78
|
+
tools = tools.filter((tool) => tool.readOnly);
|
|
79
|
+
}
|
|
80
|
+
return tools;
|
|
81
|
+
}
|
|
82
|
+
systemFor(agent, tools, depth) {
|
|
83
|
+
return {
|
|
84
|
+
role: "system",
|
|
85
|
+
content: systemPrompt(this.deps.root, {
|
|
86
|
+
agent,
|
|
87
|
+
toolNames: tools.map((tool) => tool.name),
|
|
88
|
+
instructions: this.deps.config.instructions || [],
|
|
89
|
+
subagent: depth > 0,
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// Texto que se añade al principio del siguiente mensaje tuyo (por ejemplo la salida de un !comando)
|
|
94
|
+
addNote(text) {
|
|
95
|
+
this.pendingNote += text;
|
|
96
|
+
}
|
|
97
|
+
toolContext(agent, signal, depth) {
|
|
98
|
+
const lsp = this.deps.lsp;
|
|
99
|
+
const ctx = {
|
|
100
|
+
root: this.deps.root,
|
|
101
|
+
bashTimeoutMs: this.deps.config.bashTimeoutMs,
|
|
102
|
+
signal,
|
|
103
|
+
ask: (request) => this.deps.ask(request, agent),
|
|
104
|
+
readTimes: this.readTimes,
|
|
105
|
+
};
|
|
106
|
+
if (lsp && lsp.enabled()) {
|
|
107
|
+
ctx.diagnostics = (file, all) => lsp.report(file, all);
|
|
108
|
+
}
|
|
109
|
+
if (depth === 0) {
|
|
110
|
+
ctx.todos = {
|
|
111
|
+
get: () => this.session.todos,
|
|
112
|
+
set: (list) => {
|
|
113
|
+
this.session.todos = list;
|
|
114
|
+
this.events.onTodos?.(list);
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
ctx.subagent = (type, prompt, description) => this.runSubagent(type, prompt, description, agent, signal);
|
|
118
|
+
}
|
|
119
|
+
return ctx;
|
|
120
|
+
}
|
|
121
|
+
requestTokens(messages, tools) {
|
|
122
|
+
return estimateTokens(messages) + estimateTokens(toolDefinitions(tools));
|
|
123
|
+
}
|
|
124
|
+
limit() {
|
|
125
|
+
return Math.min(this.deps.config.autoCompactTokens, Math.max(4000, this.deps.router.maxContext() * 0.8));
|
|
126
|
+
}
|
|
127
|
+
tooBig(tools) {
|
|
128
|
+
return this.requestTokens(this.fullMessages(tools), tools) > this.limit();
|
|
129
|
+
}
|
|
130
|
+
fullMessages(tools) {
|
|
131
|
+
return [this.systemFor(this.agent, tools, 0), ...this.session.messages];
|
|
132
|
+
}
|
|
133
|
+
// Vacía las salidas de herramientas antiguas (menos las últimas 6); devuelve true si ha recortado algo
|
|
134
|
+
prune(messages) {
|
|
135
|
+
const toolIndexes = [];
|
|
136
|
+
messages.forEach((message, index) => {
|
|
137
|
+
if (message.role === "tool") {
|
|
138
|
+
toolIndexes.push(index);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
let changed = false;
|
|
142
|
+
for (const index of toolIndexes.slice(0, -6)) {
|
|
143
|
+
const message = messages[index];
|
|
144
|
+
if (typeof message.content === "string" && message.content.length > 500 && !message.content.includes("[… salida antigua recortada")) {
|
|
145
|
+
message.content = `${message.content.slice(0, 300)}\n[… salida antigua recortada para ahorrar contexto …]`;
|
|
146
|
+
changed = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return changed;
|
|
150
|
+
}
|
|
151
|
+
// Antes de mandar: primero recortamos salidas viejas y, si no basta, resumimos
|
|
152
|
+
async ensureRoom(tools, signal) {
|
|
153
|
+
if (!this.tooBig(tools)) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
if (this.prune(this.session.messages) && !this.tooBig(tools)) {
|
|
157
|
+
this.events.onInfo?.("He recortado salidas antiguas de herramientas para ahorrar contexto.");
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
// Recién resumida no hay nada más que ganar (el router avisará si no cabe en ningún modelo)
|
|
161
|
+
if (this.session.messages.length <= 3) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
await this.compact(signal);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
// Resume la conversación para que quepa en modelos con poco contexto
|
|
168
|
+
async compact(signal) {
|
|
169
|
+
if (this.session.messages.length < 2) {
|
|
170
|
+
this.events.onInfo?.("No hay nada que resumir.");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.events.onInfo?.("Resumiendo la conversación…");
|
|
174
|
+
// Recortamos salidas de herramientas antiguas para que el propio resumen quepa
|
|
175
|
+
const trimmed = this.session.messages.map((m) => {
|
|
176
|
+
if (m.role === "tool" && typeof m.content === "string") {
|
|
177
|
+
return { ...m, content: truncate(m.content, 2000) };
|
|
178
|
+
}
|
|
179
|
+
return m;
|
|
180
|
+
});
|
|
181
|
+
const result = await this.deps.router.chat({
|
|
182
|
+
messages: [
|
|
183
|
+
{ role: "system", content: "You are a precise assistant that writes handover summaries." },
|
|
184
|
+
...trimmed,
|
|
185
|
+
{ role: "user", content: COMPACT_PROMPT },
|
|
186
|
+
],
|
|
187
|
+
}, signal, { onNotice: (t) => this.events.onNotice?.(t) });
|
|
188
|
+
const summary = result.message.content || "";
|
|
189
|
+
let todoText = "";
|
|
190
|
+
if (this.session.todos.length > 0) {
|
|
191
|
+
todoText = `\n\nLista de tareas actual:\n${this.session.todos.map((todo) => `- [${todo.status}] ${todo.content}`).join("\n")}`;
|
|
192
|
+
}
|
|
193
|
+
this.session.messages = [
|
|
194
|
+
{ role: "user", content: `Resumen de la conversación anterior:\n\n${summary}${todoText}` },
|
|
195
|
+
{ role: "assistant", content: "Entendido, sigo desde aquí." },
|
|
196
|
+
];
|
|
197
|
+
this.session.epoch++;
|
|
198
|
+
saveSession(this.session);
|
|
199
|
+
this.events.onInfo?.(`Conversación resumida (~${estimateTokens(this.session.messages)} tokens).`);
|
|
200
|
+
}
|
|
201
|
+
// Si se canceló a mitad, puede haber llamadas a herramientas sin respuesta: los proveedores lo rechazan
|
|
202
|
+
repairHistory(messages = this.session.messages) {
|
|
203
|
+
for (let i = 0; i < messages.length; i++) {
|
|
204
|
+
const m = messages[i];
|
|
205
|
+
if (m.role !== "assistant" || !Array.isArray(m.tool_calls)) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
for (const call of m.tool_calls) {
|
|
209
|
+
const answered = messages.some((other) => other.role === "tool" && other.tool_call_id === call.id);
|
|
210
|
+
if (!answered) {
|
|
211
|
+
// Insertamos la respuesta justo después de las que ya hay de este turno
|
|
212
|
+
let insertAt = i + 1;
|
|
213
|
+
while (insertAt < messages.length && messages[insertAt].role === "tool") {
|
|
214
|
+
insertAt++;
|
|
215
|
+
}
|
|
216
|
+
messages.splice(insertAt, 0, { role: "tool", tool_call_id: call.id, content: "Cancelado por el usuario." });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
buildUserMessage(input) {
|
|
222
|
+
const mentions = expandMentions(input.text, this.deps.root, this.readTimes);
|
|
223
|
+
for (const file of mentions.files) {
|
|
224
|
+
this.events.onInfo?.(` + adjunto ${file}`);
|
|
225
|
+
}
|
|
226
|
+
const text = this.pendingNote + mentions.text;
|
|
227
|
+
this.pendingNote = "";
|
|
228
|
+
const images = [...(input.images || []), ...mentions.images];
|
|
229
|
+
if (images.length === 0) {
|
|
230
|
+
return { role: "user", content: text };
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
role: "user",
|
|
234
|
+
content: [{ type: "text", text }, ...images.map((url) => ({ type: "image_url", image_url: { url } }))],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
// Una llamada al modelo, con el texto y el razonamiento en directo
|
|
238
|
+
async step(messages, tools, agent, signal, depth) {
|
|
239
|
+
let printedText = false;
|
|
240
|
+
const result = await this.deps.router.chat({ messages, tools: toolDefinitions(tools), temperature: agent.temperature, prefer: agent.model }, signal, {
|
|
241
|
+
onText: (chunk) => {
|
|
242
|
+
printedText = true;
|
|
243
|
+
this.events.onText?.(chunk, depth);
|
|
244
|
+
},
|
|
245
|
+
onReasoning: (chunk) => this.events.onReasoning?.(chunk, depth),
|
|
246
|
+
onNotice: (notice) => this.events.onNotice?.(notice),
|
|
247
|
+
onAttempt: (slot) => this.events.onModel?.(slot, depth),
|
|
248
|
+
});
|
|
249
|
+
if (printedText) {
|
|
250
|
+
this.events.onTextEnd?.(depth);
|
|
251
|
+
}
|
|
252
|
+
let tokens = this.requestTokens(messages, tools);
|
|
253
|
+
if (result.usage) {
|
|
254
|
+
tokens = result.usage.prompt + result.usage.completion;
|
|
255
|
+
if (depth === 0) {
|
|
256
|
+
this.session.usage.prompt += result.usage.prompt;
|
|
257
|
+
this.session.usage.completion += result.usage.completion;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (depth === 0) {
|
|
261
|
+
this.lastSlot = result.slot.key;
|
|
262
|
+
this.lastTokens = tokens;
|
|
263
|
+
}
|
|
264
|
+
this.events.onStep?.({ slot: result.slot.key, usage: result.usage, tokens });
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
// Ejecuta las herramientas que pidió el modelo; varios "task" seguidos van en paralelo
|
|
268
|
+
async runCalls(calls, tools, ctx, agent, depth, messages, signal) {
|
|
269
|
+
const images = [];
|
|
270
|
+
const runOne = async (call) => {
|
|
271
|
+
const name = call.function.name;
|
|
272
|
+
const args = call.function.arguments;
|
|
273
|
+
const event = { id: call.id, name, summary: describeCall(name, args), depth, agent: agent.name };
|
|
274
|
+
this.events.onToolStart?.(event);
|
|
275
|
+
const result = await runTool(tools, name, args, { ...ctx, onProgress: (text) => this.events.onToolProgress?.(event, text) }, agent.readOnly);
|
|
276
|
+
this.events.onToolEnd?.({ ...event, output: result.output, display: result.display, isError: Boolean(result.isError) });
|
|
277
|
+
if (result.images) {
|
|
278
|
+
images.push(...result.images);
|
|
279
|
+
}
|
|
280
|
+
return { role: "tool", tool_call_id: call.id, content: result.output };
|
|
281
|
+
};
|
|
282
|
+
const functionCalls = calls.filter((call) => call.type === "function");
|
|
283
|
+
let i = 0;
|
|
284
|
+
while (i < functionCalls.length) {
|
|
285
|
+
if (signal?.aborted) {
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
if (functionCalls[i].function.name === "task") {
|
|
289
|
+
let j = i;
|
|
290
|
+
while (j < functionCalls.length && functionCalls[j].function.name === "task") {
|
|
291
|
+
j++;
|
|
292
|
+
}
|
|
293
|
+
const results = await Promise.all(functionCalls.slice(i, j).map(runOne));
|
|
294
|
+
messages.push(...results);
|
|
295
|
+
i = j;
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
messages.push(await runOne(functionCalls[i]));
|
|
299
|
+
i++;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (images.length > 0) {
|
|
303
|
+
messages.push({
|
|
304
|
+
role: "user",
|
|
305
|
+
content: [{ type: "text", text: "Imágenes devueltas por las herramientas:" }, ...images.map((url) => ({ type: "image_url", image_url: { url } }))],
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Detecta cuando el modelo repite la misma llamada una y otra vez
|
|
310
|
+
repeated(history, calls) {
|
|
311
|
+
for (const call of calls) {
|
|
312
|
+
if (call.type !== "function") {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
history.push(`${call.function.name}:${call.function.arguments}`);
|
|
316
|
+
}
|
|
317
|
+
if (history.length >= 3) {
|
|
318
|
+
const last = history.slice(-3);
|
|
319
|
+
if (last[0] === last[1] && last[1] === last[2]) {
|
|
320
|
+
history.length = 0;
|
|
321
|
+
return last[0].split(":")[0];
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
async run(input, signal) {
|
|
327
|
+
this.running = true;
|
|
328
|
+
let turn = null;
|
|
329
|
+
try {
|
|
330
|
+
this.repairHistory();
|
|
331
|
+
const tools = this.toolsFor(this.agent, 0);
|
|
332
|
+
await this.ensureRoom(tools, signal);
|
|
333
|
+
const before = this.deps.snapshots ? await this.deps.snapshots.track() : null;
|
|
334
|
+
beginTurn();
|
|
335
|
+
turn = { messageIndex: this.session.messages.length, before, after: null, epoch: this.session.epoch, userText: input.text };
|
|
336
|
+
this.session.turns.push(turn);
|
|
337
|
+
this.session.redo = [];
|
|
338
|
+
this.session.agent = this.agent.name;
|
|
339
|
+
this.session.messages.push(this.buildUserMessage(input));
|
|
340
|
+
saveSession(this.session);
|
|
341
|
+
await this.loop(tools, signal);
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
if (turn && this.deps.snapshots) {
|
|
345
|
+
turn.after = await this.deps.snapshots.track();
|
|
346
|
+
}
|
|
347
|
+
saveSession(this.session);
|
|
348
|
+
this.running = false;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async loop(tools, signal) {
|
|
352
|
+
const agent = this.agent;
|
|
353
|
+
const maxSteps = agent.maxSteps || this.deps.config.maxSteps;
|
|
354
|
+
const ctx = this.toolContext(agent, signal, 0);
|
|
355
|
+
const history = [];
|
|
356
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
357
|
+
// Si la conversación crece demasiado a mitad de tarea, la resumimos y seguimos
|
|
358
|
+
if (await this.ensureRoom(tools, signal)) {
|
|
359
|
+
this.session.messages.push({ role: "user", content: "Continúa con la tarea pendiente." });
|
|
360
|
+
}
|
|
361
|
+
const result = await this.step(this.fullMessages(tools), tools, agent, signal, 0);
|
|
362
|
+
const message = result.message;
|
|
363
|
+
this.session.messages.push(message);
|
|
364
|
+
saveSession(this.session);
|
|
365
|
+
if (!message.tool_calls || message.tool_calls.length === 0) {
|
|
366
|
+
if (this.events.onAnswer) {
|
|
367
|
+
this.events.onAnswer(result.slot.key);
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
this.events.onInfo?.(` ${result.slot.key}`);
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
await this.runCalls(message.tool_calls, tools, ctx, agent, 0, this.session.messages, signal);
|
|
375
|
+
saveSession(this.session);
|
|
376
|
+
if (signal?.aborted) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const looping = this.repeated(history, message.tool_calls);
|
|
380
|
+
if (looping) {
|
|
381
|
+
this.session.messages.push({
|
|
382
|
+
role: "user",
|
|
383
|
+
content: `Has hecho la misma llamada a ${looping} tres veces seguidas con los mismos argumentos. No la repitas: cambia de enfoque o explica qué te bloquea.`,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
// Lo que hayas escrito mientras trabajaba se le entrega ahora
|
|
387
|
+
if (this.queue.length > 0) {
|
|
388
|
+
for (const queued of this.queue.splice(0)) {
|
|
389
|
+
this.session.messages.push(this.buildUserMessage({ text: `[Mensaje nuevo del usuario mientras trabajabas] ${queued.text}`, images: queued.images }));
|
|
390
|
+
}
|
|
391
|
+
this.events.onInfo?.("(mensaje entregado al agente)");
|
|
392
|
+
saveSession(this.session);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
this.events.onNotice?.(`Se alcanzó el máximo de ${maxSteps} pasos. Escribe "continúa" para seguir.`);
|
|
396
|
+
}
|
|
397
|
+
// Un subagente trabaja con su propio historial y devuelve solo su informe final
|
|
398
|
+
async runSubagent(type, prompt, description, parent, signal) {
|
|
399
|
+
let sub = subagents(this.deps.agents).find((agent) => agent.name === type);
|
|
400
|
+
if (!sub) {
|
|
401
|
+
sub = subagents(this.deps.agents).find((agent) => agent.name === "general");
|
|
402
|
+
}
|
|
403
|
+
if (!sub) {
|
|
404
|
+
return `Error: no existe el subagente ${type}.`;
|
|
405
|
+
}
|
|
406
|
+
// Un agente de solo lectura no puede saltarse la norma lanzando un subagente que edite
|
|
407
|
+
const agent = parent.readOnly ? { ...sub, readOnly: true } : sub;
|
|
408
|
+
const tools = this.toolsFor(agent, 1);
|
|
409
|
+
const ctx = this.toolContext(agent, signal, 1);
|
|
410
|
+
const messages = [{ role: "user", content: prompt }];
|
|
411
|
+
const maxSteps = agent.maxSteps || Math.min(this.deps.config.maxSteps, 30);
|
|
412
|
+
const history = [];
|
|
413
|
+
this.events.onInfo?.(` ↳ ${agent.name}: ${description}`);
|
|
414
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
415
|
+
const system = this.systemFor(agent, tools, 1);
|
|
416
|
+
if (this.requestTokens([system, ...messages], tools) > this.limit()) {
|
|
417
|
+
this.prune(messages);
|
|
418
|
+
}
|
|
419
|
+
const result = await this.step([system, ...messages], tools, agent, signal, 1);
|
|
420
|
+
messages.push(result.message);
|
|
421
|
+
if (!result.message.tool_calls || result.message.tool_calls.length === 0) {
|
|
422
|
+
return result.message.content || "(el subagente no devolvió ningún texto)";
|
|
423
|
+
}
|
|
424
|
+
await this.runCalls(result.message.tool_calls, tools, ctx, agent, 1, messages, signal);
|
|
425
|
+
if (signal?.aborted) {
|
|
426
|
+
return "Cancelado por el usuario.";
|
|
427
|
+
}
|
|
428
|
+
const looping = this.repeated(history, result.message.tool_calls);
|
|
429
|
+
if (looping) {
|
|
430
|
+
messages.push({ role: "user", content: `Has repetido la misma llamada a ${looping} tres veces. Cambia de enfoque o termina con lo que tengas.` });
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
this.repairHistory(messages);
|
|
434
|
+
const final = await this.deps.router.chat({
|
|
435
|
+
messages: [
|
|
436
|
+
this.systemFor(agent, tools, 1),
|
|
437
|
+
...messages,
|
|
438
|
+
{ role: "user", content: "Se acabaron los pasos. Escribe ya tu informe final con lo que hayas averiguado o hecho." },
|
|
439
|
+
],
|
|
440
|
+
}, signal, { onNotice: (notice) => this.events.onNotice?.(notice) });
|
|
441
|
+
return final.message.content || "(el subagente no devolvió ningún informe)";
|
|
442
|
+
}
|
|
443
|
+
// Deshace el último mensaje: ficheros (con instantáneas, incluido lo que hizo bash) y conversación
|
|
444
|
+
async undo() {
|
|
445
|
+
const turn = this.session.turns.pop();
|
|
446
|
+
if (!turn) {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
let files = [];
|
|
450
|
+
if (turn.before && this.deps.snapshots) {
|
|
451
|
+
files = await this.deps.snapshots.restore(turn.before);
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
files = undoLastTurn(this.deps.root);
|
|
455
|
+
}
|
|
456
|
+
let removed = [];
|
|
457
|
+
let restoredMessages = false;
|
|
458
|
+
if (turn.epoch === this.session.epoch && turn.messageIndex <= this.session.messages.length) {
|
|
459
|
+
removed = this.session.messages.splice(turn.messageIndex);
|
|
460
|
+
restoredMessages = true;
|
|
461
|
+
}
|
|
462
|
+
else if (files.length > 0) {
|
|
463
|
+
this.pendingNote += `(Nota: el usuario ha deshecho tus últimos cambios en ${files.join(", ")}; esos ficheros vuelven a estar como antes. Léelos de nuevo si los necesitas.)\n\n`;
|
|
464
|
+
}
|
|
465
|
+
this.session.redo.push({ turn, messages: removed });
|
|
466
|
+
this.readTimes.clear();
|
|
467
|
+
saveSession(this.session);
|
|
468
|
+
return { files, text: turn.userText, restoredMessages };
|
|
469
|
+
}
|
|
470
|
+
async redo() {
|
|
471
|
+
const entry = this.session.redo.pop();
|
|
472
|
+
if (!entry) {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
let files = [];
|
|
476
|
+
if (entry.turn.after && this.deps.snapshots) {
|
|
477
|
+
files = await this.deps.snapshots.restore(entry.turn.after);
|
|
478
|
+
}
|
|
479
|
+
if (entry.messages.length > 0 && entry.turn.epoch === this.session.epoch && this.session.messages.length === entry.turn.messageIndex) {
|
|
480
|
+
this.session.messages.push(...entry.messages);
|
|
481
|
+
}
|
|
482
|
+
this.session.turns.push(entry.turn);
|
|
483
|
+
this.readTimes.clear();
|
|
484
|
+
saveSession(this.session);
|
|
485
|
+
return { files };
|
|
486
|
+
}
|
|
487
|
+
// Diff de lo que ha cambiado en el último mensaje o en toda la sesión
|
|
488
|
+
async diff(all) {
|
|
489
|
+
if (!this.deps.snapshots) {
|
|
490
|
+
return "";
|
|
491
|
+
}
|
|
492
|
+
const turns = this.session.turns.filter((turn) => turn.before);
|
|
493
|
+
if (turns.length === 0) {
|
|
494
|
+
return "";
|
|
495
|
+
}
|
|
496
|
+
if (all) {
|
|
497
|
+
return this.deps.snapshots.diff(turns[0].before);
|
|
498
|
+
}
|
|
499
|
+
const last = turns[turns.length - 1];
|
|
500
|
+
return this.deps.snapshots.diff(last.before, last.after || undefined);
|
|
501
|
+
}
|
|
502
|
+
}
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readMarkdownDir } from "./frontmatter.js";
|
|
3
|
+
import { wildcardMatch } from "./match.js";
|
|
4
|
+
import { GLOBAL_AGENTS_DIR } from "./paths.js";
|
|
5
|
+
const PLAN_PROMPT = `You are in PLAN mode: you can only read and search (and browse the web). Do NOT try to modify anything.
|
|
6
|
+
Investigate the code and reply with a concrete, step-by-step plan: which files to change, what to change in each one and how to verify it.`;
|
|
7
|
+
const GENERAL_PROMPT = `You are a subagent launched by the main agent to do one task on your own.
|
|
8
|
+
Work autonomously until the task is done; you cannot ask the user questions.
|
|
9
|
+
Your final answer is the only thing the main agent will see: make it a complete, self-contained report (what you found or changed, with file paths and key details).`;
|
|
10
|
+
const EXPLORE_PROMPT = `You are a fast, read-only exploration subagent.
|
|
11
|
+
Use glob, grep, list_dir and read_file to answer the question about the codebase. Be efficient: search first, then read only what you need.
|
|
12
|
+
Your final answer is the only thing the main agent will see: give precise findings with file paths and line numbers.`;
|
|
13
|
+
const BUILTIN = [
|
|
14
|
+
{
|
|
15
|
+
name: "build",
|
|
16
|
+
description: "Agente principal: lee, edita ficheros y ejecuta comandos",
|
|
17
|
+
mode: "primary",
|
|
18
|
+
prompt: "",
|
|
19
|
+
tools: {},
|
|
20
|
+
readOnly: false,
|
|
21
|
+
color: "magenta",
|
|
22
|
+
builtIn: true,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "plan",
|
|
26
|
+
description: "Solo lectura: investiga y propone un plan sin tocar nada",
|
|
27
|
+
mode: "primary",
|
|
28
|
+
prompt: PLAN_PROMPT,
|
|
29
|
+
tools: {},
|
|
30
|
+
permission: { edit: "deny", bash: "deny" },
|
|
31
|
+
readOnly: true,
|
|
32
|
+
color: "blue",
|
|
33
|
+
builtIn: true,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "general",
|
|
37
|
+
description: "Subagente para investigar preguntas complejas o hacer tareas de varios pasos de forma autónoma (puede editar)",
|
|
38
|
+
mode: "subagent",
|
|
39
|
+
prompt: GENERAL_PROMPT,
|
|
40
|
+
tools: { task: false, todowrite: false, todoread: false },
|
|
41
|
+
readOnly: false,
|
|
42
|
+
color: "cyan",
|
|
43
|
+
builtIn: true,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "explore",
|
|
47
|
+
description: "Subagente rápido de solo lectura para encontrar ficheros, símbolos o entender cómo funciona algo del código",
|
|
48
|
+
mode: "subagent",
|
|
49
|
+
prompt: EXPLORE_PROMPT,
|
|
50
|
+
tools: { task: false, todowrite: false, todoread: false, webfetch: false, websearch: false },
|
|
51
|
+
readOnly: true,
|
|
52
|
+
color: "green",
|
|
53
|
+
builtIn: true,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
// Nombres de herramientas de opencode → los de vipcode
|
|
57
|
+
const ALIASES = {
|
|
58
|
+
read: "read_file",
|
|
59
|
+
write: "write_file",
|
|
60
|
+
edit: "edit_file",
|
|
61
|
+
patch: "edit_file",
|
|
62
|
+
multiedit: "edit_file",
|
|
63
|
+
list: "list_dir",
|
|
64
|
+
ls: "list_dir",
|
|
65
|
+
};
|
|
66
|
+
const COLORS = ["magenta", "blue", "cyan", "green", "yellow", "red", "white", "gray"];
|
|
67
|
+
function normalizeTools(input) {
|
|
68
|
+
const result = {};
|
|
69
|
+
if (Array.isArray(input)) {
|
|
70
|
+
// Lista = solo estas herramientas
|
|
71
|
+
result["*"] = false;
|
|
72
|
+
for (const name of input) {
|
|
73
|
+
result[ALIASES[String(name)] || String(name)] = true;
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
if (input && typeof input === "object") {
|
|
78
|
+
for (const [name, enabled] of Object.entries(input)) {
|
|
79
|
+
result[ALIASES[name] || name] = Boolean(enabled);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
function fromInput(name, input, base, source) {
|
|
85
|
+
let mode = input.mode || base?.mode || "all";
|
|
86
|
+
if (!["primary", "subagent", "all"].includes(mode)) {
|
|
87
|
+
mode = "all";
|
|
88
|
+
}
|
|
89
|
+
let color = input.color || base?.color || "yellow";
|
|
90
|
+
if (!COLORS.includes(color)) {
|
|
91
|
+
color = "yellow";
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
name,
|
|
95
|
+
description: input.description || base?.description || `Agente ${name}`,
|
|
96
|
+
mode,
|
|
97
|
+
prompt: input.prompt ?? base?.prompt ?? "",
|
|
98
|
+
model: input.model || base?.model,
|
|
99
|
+
temperature: typeof input.temperature === "number" ? input.temperature : base?.temperature,
|
|
100
|
+
tools: { ...(base?.tools || {}), ...normalizeTools(input.tools) },
|
|
101
|
+
permission: input.permission || base?.permission,
|
|
102
|
+
maxSteps: typeof input.maxSteps === "number" ? input.maxSteps : base?.maxSteps,
|
|
103
|
+
readOnly: typeof input.readOnly === "boolean" ? input.readOnly : base?.readOnly ?? false,
|
|
104
|
+
color,
|
|
105
|
+
builtIn: base?.builtIn ?? false,
|
|
106
|
+
source: source || base?.source,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// Agentes integrados + ~/.vipcode/agents + .vipcode/agents y .opencode/agent(s) del proyecto + "agents" del config
|
|
110
|
+
export function loadAgents(root, config) {
|
|
111
|
+
const agents = new Map();
|
|
112
|
+
for (const agent of BUILTIN) {
|
|
113
|
+
agents.set(agent.name, { ...agent, tools: { ...agent.tools } });
|
|
114
|
+
}
|
|
115
|
+
const dirs = [
|
|
116
|
+
GLOBAL_AGENTS_DIR,
|
|
117
|
+
path.join(root, ".opencode", "agent"),
|
|
118
|
+
path.join(root, ".opencode", "agents"),
|
|
119
|
+
path.join(root, ".vipcode", "agents"),
|
|
120
|
+
];
|
|
121
|
+
const disabled = new Set();
|
|
122
|
+
const apply = (name, input, source) => {
|
|
123
|
+
if (input.disable) {
|
|
124
|
+
disabled.add(name);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
agents.set(name, fromInput(name, input, agents.get(name), source));
|
|
128
|
+
};
|
|
129
|
+
for (const entry of readMarkdownDir(dirs)) {
|
|
130
|
+
const data = entry.data;
|
|
131
|
+
apply(entry.name, {
|
|
132
|
+
description: data.description,
|
|
133
|
+
mode: data.mode,
|
|
134
|
+
prompt: entry.body,
|
|
135
|
+
model: data.model,
|
|
136
|
+
temperature: data.temperature,
|
|
137
|
+
tools: data.tools,
|
|
138
|
+
permission: data.permission,
|
|
139
|
+
maxSteps: data.maxSteps ?? data.steps,
|
|
140
|
+
color: data.color,
|
|
141
|
+
readOnly: data.readOnly,
|
|
142
|
+
disable: data.disable,
|
|
143
|
+
}, entry.file);
|
|
144
|
+
}
|
|
145
|
+
for (const [name, input] of Object.entries(config.agents || {})) {
|
|
146
|
+
apply(name, input);
|
|
147
|
+
}
|
|
148
|
+
for (const name of disabled) {
|
|
149
|
+
agents.delete(name);
|
|
150
|
+
}
|
|
151
|
+
if (!agents.has("build")) {
|
|
152
|
+
agents.set("build", { ...BUILTIN[0] });
|
|
153
|
+
}
|
|
154
|
+
return [...agents.values()];
|
|
155
|
+
}
|
|
156
|
+
export function primaryAgents(agents) {
|
|
157
|
+
return agents.filter((agent) => agent.mode === "primary" || agent.mode === "all");
|
|
158
|
+
}
|
|
159
|
+
export function subagents(agents) {
|
|
160
|
+
return agents.filter((agent) => agent.mode === "subagent" || agent.mode === "all");
|
|
161
|
+
}
|
|
162
|
+
// ¿Puede este agente usar la herramienta? Gana la última regla que encaje; por defecto sí
|
|
163
|
+
export function toolEnabled(agent, toolName) {
|
|
164
|
+
let enabled = true;
|
|
165
|
+
for (const [pattern, value] of Object.entries(agent.tools)) {
|
|
166
|
+
if (pattern === toolName || wildcardMatch(pattern, toolName)) {
|
|
167
|
+
enabled = value;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return enabled;
|
|
171
|
+
}
|