Spaces:
Running
Running
File size: 9,264 Bytes
b30b7c5 4bd8362 b30b7c5 d305959 b30b7c5 4bd8362 b30b7c5 4bd8362 b30b7c5 4bd8362 b30b7c5 4bd8362 b30b7c5 4bd8362 b30b7c5 4bd8362 b30b7c5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | // Registro de herramientas de Elffuss Code: proyecto abierto + shell + web.
import * as code from './code.js';
import * as shell from '../shell.js';
import * as web from './web.js';
import { anunciar, tocaFichero } from '../actividad.js';
export { code };
// El terminal (UI) se engancha aquí para reflejar lo que ejecuta la elfa.
let onTerminalEcho = () => {};
export function setTerminalEcho(fn) { onTerminalEcho = fn; }
export const TOOLS = {
'code.tree': { desc: 'Ver el árbol de archivos del proyecto', params: { path: 'subcarpeta (opcional)', depth: 'niveles (3)' }, run: a => code.tree(a) },
'code.read': { desc: 'Leer un archivo del proyecto. Sin más parámetros, lo lee ENTERO. Para ficheros grandes, pagina de 100 en 100 líneas con offset (offset:101 = siguiente página tras la primera). Para ir directo a donde dio un resultado de code.search (que ya incluye número de línea), usa around:N — trae ~100 líneas centradas en la línea N, con cada línea numerada (N→contenido) para poder citarla o editarla con precisión', params: { path: 'ruta relativa', offset: 'línea 1-based donde empezar (opcional)', limit: 'líneas a devolver, 100 por defecto (opcional)', around: 'centra la página en esta línea, p.ej. la que dio code.search (opcional)' }, run: a => code.read(a) },
'code.write': { desc: 'Escribir/crear un archivo (contenido COMPLETO; se refleja al instante en el editor). Para cambios PEQUEÑOS en un fichero YA existente, usa mejor code.edit', params: { path: 'ruta', content: 'contenido íntegro' }, run: a => code.write(a) },
'code.edit': { desc: 'Editar PARTE de un fichero existente sin reescribirlo entero: sustituye "search" (copia literal de unas pocas líneas, con contexto suficiente para ser única en el fichero) por "replace". Preferible a code.write para cambios pequeños/medianos — más rápido y sin riesgo de perder el resto del fichero. Si falla (no encuentra el punto con confianza), relee el fichero y reintenta con un search más ajustado', params: { path: 'ruta', search: 'texto exacto a sustituir', replace: 'texto nuevo' }, run: a => code.edit(a) },
'code.run': { desc: 'EJECUTAR una expresión contra un módulo JS del proyecto y ver qué devuelve de verdad. Úsalo SIEMPRE después de arreglar algo, para comprobar que el arreglo funciona antes de darlo por bueno: code.run con path del fichero y expr como "max([])" — las funciones exportadas se llaman por su nombre a secas, sin import ni require (también valen como m.max). Corre aislado y con límite de tiempo', params: { path: 'ruta del .js', expr: 'expresión a evaluar, p.ej. m.suma(2,3)' }, run: a => code.run(a) },
'code.search': { desc: 'Buscar texto en el proyecto (grep)', params: { query: 'texto', ext: 'filtro extensión (opcional)' }, run: a => code.search(a) },
'terminal.run': { desc: 'Ejecutar un comando de shell sobre los ficheros del proyecto (ls, cat, grep, find, mkdir, echo>fichero, git status…). node/npm/python REALES si el usuario tiene un Bridge local conectado (⚙ Ajustes → 🔌 Bridge local); si no está conectado, el propio resultado lo indica', params: { command: 'la línea de comando' }, run: async (a, ctx = {}) => { const out = await shell.runForAgent(a.command || '', { signal: ctx.signal }); onTerminalEcho(a.command || '', out); return out; } },
'web.search': { desc: 'Buscar en internet (docs, errores, APIs) — devuelve títulos, URLs y fragmentos', params: { query: 'qué buscar' }, run: a => web.search(a) },
'web.fetch': { desc: 'Leer el contenido de texto de una URL (documentación, referencia)', params: { url: 'https://…' }, run: a => web.fetchUrl(a) },
};
// Cómo llega el valor de un <param> cuando el modelo usa etiquetas.
// ─────────────────────────────────────────────────────────────────────────────
// Por defecto se RECORTA, porque el modelo escribe el valor en su propia línea
// («<param name="path">\n src/a.js\n</param>») y ese espacio es formato suyo,
// no parte de la ruta. Pero hay dos excepciones, y una se nos escapó en
// producción desde b7d7db0:
//
// · EXACTO: el valor ES el fichero, o el texto que tiene que casar con él
// byte a byte. Recortarlo deja la primera línea sin sangría y el fichero sin
// salto final. En `code.edit` es peor que cosmético: `search` se compara con
// el contenido real, así que un trim hace que no case y la edición falla EN
// SILENCIO.
// · NÚMERO: el valor es una cuenta de líneas; el resto del código la usa como
// número y no como la cadena "100".
//
// Lo que NO está aquí se recorta, y nada se pasa por `JSON.parse`: un fichero
// .json es texto, no un objeto, y un valor entrecomillado conserva sus comillas.
// `tests/funciontags.mjs` comprueba que cada nombre de aquí es un parámetro de
// verdad, para que renombrar uno no deje la excepción apuntando al aire.
export const PARAM_EXACTO = {
'code.write': ['content'],
'code.edit': ['search', 'replace'],
};
export const PARAM_NUMERO = {
'code.tree': ['depth'],
'code.read': ['offset', 'limit', 'around'],
};
export function toolHelp() {
return Object.entries(TOOLS).map(([n, t]) =>
`- ${n}(${Object.keys(t.params).join(', ')}): ${t.desc}`).join('\n');
}
// `ctx` lo pone el agente: hoy, la señal de parada de su turno ({ signal }), que
// terminal.run pasa al bridge para matar el comando si se para el agente.
export async function runTool(name, args, ctx = {}) {
const tool = TOOLS[name];
if (!tool) throw new Error(`Herramienta desconocida: ${name}`);
// Se anuncia para que la vista de ciudad y su pila lo vean en directo. Si la
// herramienta falla, se anuncia el fallo y el error sigue su camino normal.
const quien = tocaFichero(name, args);
anunciar({ fase: 'inicio', herramienta: name, args: args || {}, ...(quien || {}) });
try {
const v = await tool.run(args || {}, ctx || {});
anunciar({ fase: 'fin', herramienta: name, ok: true, resumen: resumenDe(v), ...(quien || {}) });
return v;
} catch (e) {
anunciar({ fase: 'fin', herramienta: name, ok: false, resumen: String(e?.message || e).slice(0, 160), ...(quien || {}) });
throw e;
}
}
// Una línea para el registro. El resultado completo va al modelo; aquí solo
// se enseña para que se vea qué pasó.
const resumenDe = v => {
const t = typeof v === 'string' ? v : JSON.stringify(v ?? '');
const linea = (t || '').split('\n').find(l => l.trim()) || '';
return linea.slice(0, 160);
};
// La hora, TRUNCADA A LA HORA EN PUNTO, y el motivo no es estético.
// ─────────────────────────────────────────────────────────────────────────────
// Esto era `new Date().toLocaleString()`, que incluye SEGUNDOS, y va en la
// primera línea del contexto — o sea en la cabeza del prompt de sistema. El
// motor reutiliza el prefijo solo si los tokens del turno EMPIEZAN EXACTAMENTE
// por los que ya ingirió, así que un reloj con segundos hacía diverger el
// prefijo en el token ~5 de CADA turno: la caché de prefijo existía, estaba
// encendida, estaba medida —sin ella la espera hasta la primera letra crece
// ×3,2 del turno 1 al 6; con ella se queda plana— y no saltaba NUNCA en
// producción porque este prompt la invalidaba sola.
//
// Truncar a la hora la deja estable durante toda una sesión normal; cruzar una
// hora cuesta UN re-prefill, no uno por turno. La fecha se conserva entera
// porque el modelo la usa; los segundos no los usaba nadie —«ayer» se resuelve
// con el `ts` del turno en que se dijo (ver annotateDates en acer-core.js), no
// con este reloj—.
const horaEstable = (loc) => {
const d = new Date();
d.setMinutes(0, 0, 0);
return d.toLocaleString(loc, { dateStyle: 'short', timeStyle: 'short' });
};
// CONTEXTO AHORA del IDE: proyecto, archivo abierto y árbol resumido.
export async function snapshot() {
const { projectName, currentFile } = code.current();
const parts = [
'Fecha y hora: ' + horaEstable(),
'Proyecto abierto: ' + (projectName || 'ninguno'),
'Archivo abierto en el editor: ' + (currentFile || 'ninguno'),
];
try { parts.push('Árbol del proyecto (resumen):\n' + await code.tree({ depth: 2 })); }
catch { /* aún sin proyecto */ }
// GROUNDING: incluye el contenido REAL de los archivos clave (README, config)
// para que el modelo NO alucine sobre el proyecto — el heal no domina las
// herramientas code.* así que le damos el material masticado.
for (const key of ['README.md', 'readme.md', 'package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod']) {
try { const body = await code.read({ path: key }); parts.push(`Contenido REAL de ${key}:\n${body.slice(0, 1400)}`); break; } catch { /* siguiente */ }
}
if (currentFile) {
try {
const body = await code.read({ path: currentFile });
parts.push(`Contenido REAL del archivo abierto ${currentFile}:\n` + body.slice(0, 2000));
} catch { /* borrado */ }
}
return parts.join('\n');
}
|