|
8 min de lectura
|

Coding Agent: REPL is all you need

Resumir este artículo con:
ChatGPTPerplexityClaudeGrok

Hace un año reconocimos que un bucle de agente simple con herramientas es suficiente para tareas de programación.

El proyecto pi / pi-mono de Mario Zechner lo demuestra: un agente de código no necesita mucho. Una herramienta bash es suficiente, y su harness de ~1000 líneas ya se mantiene a la par de Claude Code y Codex — una décima parte del tamaño de los agentes en producción, y la diferencia no está donde uno esperaría.

La razón por la que funciona es que los agentes ya son buenos en dos cosas que se combinan muy bien: componer comandos de shell en pipelines y escribir código. Entonces, ¿para qué enviarlos con una caja de herramientas predefinida cuando podrían escribir las herramientas ellos mismos, justo cuando las necesitan, adaptadas al usuario, al proyecto y a la tarea en cuestión?

¿Le resulta familiar? Es Emacs de nuevo. Emacs nunca fue realmente un editor con plugins — es un entorno de ejecución Lisp que, de paso, edita texto y se reescribe a sí mismo al vuelo; por eso, cuarenta años después, la gente sigue construyendo flujos de trabajo enteros dentro de él. El mismo truco encaja en un agente de código: proporciónele un entorno de ejecución y un REPL, una sola capacidad — escribir y ejecutar código — y deténgase ahí. El resto (read, write, edit, grep, subagent, lo que surja después) el agente lo escribirá por sí solo. ¡El código se compone mucho mejor que MCP o bash!

Hyper Code

En hyper-code estoy construyendo exactamente eso — un entorno de ejecución donde el agente puede vivir y extenderse a sí mismo. Es lo suficientemente pequeño como para que el agente mantenga toda la arquitectura en su contexto, y lo suficientemente sencillo como para que pueda ampliar esa arquitectura sin romper nada.

Un esquema rápido de la forma del entorno de ejecución:

  • Bun como entorno de ejecución principal — rápido, con muchas bibliotecas integradas, sin dependencias externas.
  • Estilo procedural/funcional: estado separado de las funciones. Una función por archivo, recarga en caliente sin necesidad de reiniciar.
  • SQLite para sesiones, en el espíritu de out of the tar pit.
  • Una interfaz web con HTTP y HTML — sin reinventar la rueda con una TUI, sin la complejidad de la API de VS Code. Fácil de extender y personalizar.

Ejemplo

Para concretar esto, le pedí a Claude que propusiera una tarea realista y no trivial que un agente gestionaría en una sola llamada a evalCode. Esto fue lo que respondió. Imagine que un usuario dice: «extrae las notas de lanzamiento desde la última etiqueta — commits, autores, PRs vinculados, y guarda el resultado en caché para la próxima vez.»

En el mundo de bash + MCP, serían cinco o seis herramientas separadas que habría que describir y conectar con antelación: git describe, git log, un analizador de commits convencionales, la API de GitHub para PRs, SQLite para la caché, y formato markdown. Cada una con su propia superficie de API, cada una con un ciclo de ida y vuelta para definir y llamar.

En un agente orientado al REPL es simplemente un evalCode:

const repo = "niquola/hyper-code2";
const headers = { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` };

// 1. Last tag and commits since
const tag = (await Bun.$`git describe --tags --abbrev=0`.text()).trim();
const raw = await Bun.$`git log ${tag}..HEAD --pretty=format:%H|%s|%an`.text();

// 2. Local SQLite cache — if we've been here already, return what we have
await ctx.fns.db.exec(ctx, `
    CREATE TABLE IF NOT EXISTS release_notes (
        tag TEXT PRIMARY KEY, head TEXT, body TEXT, at INTEGER
    )
`);
const head = (await Bun.$`git rev-parse HEAD`.text()).trim();
const cached = await ctx.fns.db.select(ctx,
    "SELECT body FROM release_notes WHERE tag = ? AND head = ?", [tag, head]);
if (cached.length) {
    agent.scratchpad.release = { tag, body: cached[0].body, cached: true };
    return { since: tag, cached: true, preview: cached[0].body.slice(0, 300) + "…" };
}

// 3. Parse conventional commits: "feat(scope): message (#42)"
const commits = raw.split("\n").filter(Boolean).map(line => {
    const [sha, subject, author] = line.split("|");
    const m = subject.match(/^(feat|fix|chore|refactor|docs|test)(?:\([^)]+\))?:\s*(.+?)(?:\s*\(#(\d+)\))?$/);
    return { sha, author, type: m?.[1] ?? "other", title: m?.[2] ?? subject, pr: m?.[3] };
});

// 4. Fetch linked PRs in parallel — labels and linked issues come with them
const prNums = [...new Set(commits.map(c => c.pr).filter(Boolean))];
const prs = await Promise.all(prNums.map(n =>
    fetch(`https://api.github.com/repos/${repo}/pulls/${n}`, { headers }).then(r => r.json())
));

// 5. Group by type, build markdown
const byType = Map.groupBy(commits, c => c.type);
const body = [...byType].map(([type, cs]) =>
    `## ${type}\n${cs.map(c =>
        `- ${c.title}${c.pr ? ` (#${c.pr})` : ""} — @${c.author}`
    ).join("\n")}`
).join("\n\n");

// 6. Save the cache — good until HEAD moves
await ctx.fns.db.exec(ctx,
    "INSERT OR REPLACE INTO release_notes VALUES (?, ?, ?, ?)",
    [tag, head, body, Date.now()]);

// 7. Heavy stuff stays in scratchpad. The model gets a short summary.
agent.scratchpad.release = { tag, commits, prs, body };
return {
    since: tag,
    commits: commits.length,
    by_type: Object.fromEntries([...byType].map(([t, cs]) => [t, cs.length])),
    contributors: [...new Set(commits.map(c => c.author))],
    prs_linked: prs.length,
    labels: [...new Set(prs.flatMap(p => p.labels?.map(l => l.name) ?? []))],
    preview: body.slice(0, 300) + "…",
};

Del script puntual a su propia herramienta

La primera vez, el agente escribe esto de memoria. La segunda vez, se da cuenta de que hizo casi lo mismo la semana pasada. Y la tercera vez, en lugar de volver a escribirlo, guarda el código en un archivo y lo convierte en un procedimiento de proyecto propiamente dicho:

// Turn N: pin the pattern
await Bun.write(".hyper/release/notes.ts", `
    export default async function (ctx: Context, repo: string) {
        // ... same logic as above — now a reusable function
        return { tag, commits, prs, body };
    }
`);

// Hot-load into the live runtime and regenerate types — no restart
await ctx.fns.repl.load(ctx, "release");
await ctx.genTypes(ctx);

// Turn N+1: call it like a built-in
await ctx.fns.release.notes(ctx, "niquola/hyper-code2");

La próxima vez que alguien pida las notas de lanzamiento, será una línea en lugar de cuarenta. La función vive bajo .hyper/, lo que significa que permanece en el proyecto — y a lo largo de unas semanas la base de código crece silenciosamente con una pequeña biblioteca de procedimientos, cada uno adaptado a este repositorio específico y a este autor específico. Eso no se puede obtener de serie con ningún harness fijo, porque ningún harness fijo conoce sus hábitos.

Pruébelo

La idea en una frase: proporcione al modelo el entorno de ejecución completo y una sola herramienta, y deje que escriba el resto — adaptado a usted y a su proyecto.

El prototipo está en github.com/niquola/hyper-code2. Unas mil líneas sobre Bun: un evalCode, archivos recargados en caliente, SQLite para sesiones, un chat web en /. Funciona con OpenAI, Anthropic, Groq, OpenRouter y LM Studio local.

Si dispone de cinco minutos, pruebe esto:

  1. bun install && cp .env.test .env — introduzca la clave de su proveedor.
  2. bun src/$main.ts → abra http://localhost:3000.
  3. Pregunte: «lee tu propia base de código y explica cómo funciona.»
  4. Pregunte: «¿qué herramientas te faltan para trabajar en ...?»
  5. Pregunte: «¡escríbela y llámala!»

El proyecto todavía está en pañales. Si le gusta la idea — ¡únase a la diversión!

Compartir este artículo
Comments
Comments
Sign in
Loading comments...
Subscribe to our blog

Get the latest articles on FHIR, interoperability, and healthcare IT.