Saltar al contenido
astorlm
Idioma: Español
← Mapa

Nivel 10

Vueltas limpias

Algunos trabajos son demasiado largos para una sola sesión. Córrelos en vueltas: un agente nuevo en cada vuelta, y el progreso anotado donde el siguiente pueda encontrarlo.
1/65 Pliegues del bandoneón:
  • user
  • assistant
  • tool_result
Un cañón de 36 ladrillos de ancho, y una multitud que necesita llegar a la salida. Demasiado para la ejecución de un solo agente: este trabajo va en vueltas de 12 ladrillos.

EventBus

El problema

Algunos trabajos no entran en una sola ejecución: migrar 300 archivos, traducir un catálogo entero, arreglar cada test que falla en un repo. Cada paso suma una llamada a herramienta y un resultado al historial, y el bucle reenvía todo eso en cada turno.

Ese es Muddle, la sesión interminable. Por la tarde ya carga cada paso desde la mañana: los pedidos son enormes, los resultados viejos entierran a los nuevos, y el modelo empieza a rehacer trabajo que ya hizo o a saltarse trabajo que solo había planeado. Nada se cae. La calidad simplemente se escurre.

La compactación (nivel 7) frena a Muddle. No lo detiene: un trabajo lo bastante largo termina resumiendo sus propios resúmenes.

La solución

No mantengas vivo un solo agente durante todo el trabajo. Córrelo en vueltas. En cada vuelta, tu código arranca un agente nuevo con el historial vacío y el mismo objetivo. Hace una porción del trabajo, anota cómo quedaron las cosas y termina. Después tu código revisa el trabajo en sí y, si no está terminado, arranca la siguiente vuelta.

  • Una sesión larga

    Mantener el mismo agente y el mismo historial durante todo el trabajo.

    Cada turno reenvía todo desde el principio. Los pedidos se vuelven más pesados, al modelo le cuesta más encontrar lo que importa en ellos, y pasada la ventana se rompe.

  • Compactar sobre la marcha

    La misma sesión, pero achicando los mensajes viejos cuando el historial se acerca al límite (nivel 7).

    Gana tiempo, no lo resuelve. Cada compactación pierde detalle, y un trabajo lo bastante largo termina compactando sus propios resúmenes.

  • Vueltas limpias

    Dividir el trabajo en vueltas. Cada vuelta es un agente nuevo con el historial vacío. Lo que necesita saber, lo lee de archivos.

    Cada vuelta empieza chica y limpia. El precio: cada vuelta gasta un turno o dos en ubicarse, y los archivos tienen que decir todo lo que importa.

El truco es que nada importante vive en el historial. El trabajo está en disco (el puente), y también una nota corta que dice hasta dónde se llegó (PROGRESS.md). Un agente nuevo no necesita recordar la vuelta anterior. Solo necesita leer.

Al patrón suelen llamarlo Ralph loop, por un one-liner de shell que le pasaba a un agente de código el mismo prompt una y otra vez. Los agentes de código lo usan para refactors largos, con el árbol de git y un archivo TODO como estado.

El elenco

El mismo elenco de siempre, esta vez en un cañón.

La escotilla tu código
Arranca un agente nuevo en cada vuelta (createIterationAgent) y recibe su respuesta. Es el bucle alrededor del bucle.
El Astor de una vuelta una ejecución del agente
El bucle del agente del nivel 2, con su propio bandoneón. Empieza vacío y se va flotando cuando termina la vuelta.
El puente el trabajo
Lo que las herramientas cambiaron en disco. Ninguna vuelta lo tira.
El cartel PROGRESS.md
Una nota corta de cada vuelta para la siguiente: qué está hecho, qué viene después.
DONE? isDone
Tu verificación, entre vueltas. Mide el puente, no lo que el modelo dice de él.
LAP 3/5 maxIterations
El fusible. Si el trabajo nunca pasa la verificación, el bucle se detiene igual.

Mira las dos barras de arriba. Esta vuelta es lo que pesa de verdad cada pedido, y vuelve a empezar en cada vuelta. 1 sesión es lo que pesarían los mismos pedidos si un solo agente hubiera hecho las tres vueltas: nunca baja.

El código

Con astorlm: runGoalLoop recibe una fábrica que devuelve un agente nuevo, tu verificación isDone y un fusible maxIterations. Las herramientas escriben en archivos, así que cada vuelta encuentra el trabajo donde lo dejó la anterior.

Desde cero: El bucle del nivel 2, llamado dentro de un for. El historial es una variable local de cada llamada, así que cada vuelta empieza vacía sin costo.

import { OpenAIProvider, createLocalAgent, runGoalLoop, tool } from 'astorlm'
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { z } from 'zod'

// Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
const LLM = { baseURL: 'http://localhost:11434/v1', apiKey: 'YOUR_API_KEY' } // local servers usually ignore the key

// The state lives on disk, not in any history: the bridge, and a progress note.
const GAP = 36
const bridgeLength = (): number => (existsSync('bridge.json') ? JSON.parse(readFileSync('bridge.json', 'utf8')).length : 0)

const readProgress = tool({
  name: 'read_progress',
  description: 'Read PROGRESS.md: what earlier laps built, and where to start.',
  schema: z.object({}),
  execute: async () => (existsSync('PROGRESS.md') ? readFileSync('PROGRESS.md', 'utf8') : 'Nothing built yet.'),
})

const layBricks = tool({
  name: 'lay_bricks',
  description: 'Lay up to 12 bricks of the bridge, starting at brick number "from".',
  schema: z.object({ from: z.number().int().min(1), count: z.number().int().min(1).max(12) }),
  execute: async ({ from, count }) => {
    const to = Math.min(from + count - 1, GAP)
    writeFileSync('bridge.json', JSON.stringify({ length: Math.max(bridgeLength(), to) }))
    return `Laid bricks ${from}-${to}. The bridge is ${bridgeLength()} bricks long.`
  },
})

const writeProgress = tool({
  name: 'write_progress',
  description: 'Overwrite PROGRESS.md with where the bridge stands now, for whoever comes next.',
  schema: z.object({ text: z.string() }),
  execute: async ({ text }) => {
    writeFileSync('PROGRESS.md', `# Progress\n${text}\n`)
    return 'Saved PROGRESS.md.'
  },
})

const result = await runGoalLoop({
  goal: 'Build the bridge to the exit: 36 bricks. Read PROGRESS.md first, lay at most 12 bricks, then update PROGRESS.md.',
  // A NEW agent every lap: empty history, fresh context window. Same tools, same folder.
  createIterationAgent: () =>
    createLocalAgent({
      provider: new OpenAIProvider({ ...LLM, model: 'your-model' }), // e.g. 'llama3.1', 'gpt-4o-mini'
      tools: [readProgress, layBricks, writeProgress],
      maxTurns: 8,
    }),
  // Your code decides when the job is done, by checking the work itself. Not the model's word.
  isDone: () => bridgeLength() >= GAP,
  onIteration: ({ iteration, lastText }) => console.log(`lap ${iteration}: ${lastText}`),
  maxIterations: 5, // the fuse: a goal that never checks out can't run forever
})

console.log(result) // { iterations: 3, done: true, stopReason: 'done', lastText: '…' }

Qué vigilar

  • Verifica el trabajo, no la respuesta. Un “¡Listo!” del modelo no prueba nada. isDone debería mirar el resultado en sí: correr los tests, contar las filas, medir el puente. Mantenlo barato y determinista, porque corre después de cada vuelta.
  • Pon siempre el fusible. Una verificación que nunca puede pasar, o un agente que sigue deshaciendo su propio trabajo, da vueltas hasta que tu factura lo detiene. maxIterations, y una mirada a por qué se agotó.
  • El archivo de progreso es el único traspaso. Lo que deje afuera, la siguiente vuelta no lo sabe. Dile al agente exactamente qué escribir ahí: qué está hecho, qué sigue, qué probó y falló.
  • Haz que cada paso sea seguro de repetir. Una vuelta puede morir a la mitad, después del trabajo pero antes de la nota. La siguiente vuelta va a hacer esa porción otra vez, así que hacerla dos veces no debe romper nada.
  • Mantén las porciones chicas. Una vuelta debería entrar en una ejecución corta. Si una sola porción ya necesita compactación, las porciones son demasiado grandes.