Pular para o conteúdo
astorlm
Idioma: Português
← Mapa

Nível 10

Voltas limpas

Alguns trabalhos são longos demais para uma sessão. Rode-os em voltas: um agente novo a cada volta, e o progresso anotado onde o próximo consiga encontrar.
1/65 Dobras do bandoneón:
  • user
  • assistant
  • tool_result
Um cânion de 36 tijolos de largura, e uma multidão que precisa chegar à saída. Demais para a execução de um único agente: este trabalho vai em voltas de 12 tijolos.

EventBus

O problema

Alguns trabalhos não cabem numa execução: migrar 300 arquivos, traduzir um catálogo inteiro, corrigir cada teste quebrado de um repositório. Cada passo acrescenta uma chamada de ferramenta e um resultado ao histórico, e o loop reenvia tudo isso a cada turno.

Esse é o Muddle, a sessão sem fim. À tarde ele já carrega cada passo desde a manhã: as requisições são enormes, os resultados antigos soterram os novos, e o modelo começa a refazer trabalho que já fez ou a pular trabalho que só tinha planejado. Nada quebra. A qualidade simplesmente vai escorrendo.

A compactação (nível 7) freia o Muddle. Mas não o detém: um trabalho longo o bastante acaba resumindo os próprios resumos.

A solução

Não mantenha um único agente vivo durante o trabalho inteiro. Rode em voltas. A cada volta, o seu código inicia um agente novo com o histórico vazio e o mesmo objetivo. Ele faz uma fatia do trabalho, anota como as coisas ficaram e termina. Depois o seu código confere o trabalho em si e, se não estiver pronto, inicia a próxima volta.

  • Uma sessão longa

    Manter o mesmo agente e o mesmo histórico durante o trabalho inteiro.

    Cada turno reenvia tudo desde o começo. As requisições ficam mais pesadas, o modelo tem mais dificuldade de achar o que importa nelas, e, passada a janela, ele quebra.

  • Compactar no caminho

    A mesma sessão, mas encolhendo as mensagens antigas quando o histórico se aproxima do limite (nível 7).

    Ganha tempo, não resolve. Cada compactação perde detalhe, e um trabalho longo o bastante acaba compactando os próprios resumos.

  • Voltas limpas

    Dividir o trabalho em voltas. Cada volta é um agente novo com o histórico vazio. O que ele precisa saber, ele lê de arquivos.

    Cada volta começa pequena e limpa. O preço: cada volta gasta um ou dois turnos para se situar, e os arquivos precisam dizer tudo o que importa.

O segredo é que nada importante vive no histórico. O trabalho está no disco (a ponte), e também uma nota curta dizendo até onde se chegou (PROGRESS.md). Um agente novo não precisa lembrar da volta anterior. Só precisa ler.

O padrão costuma ser chamado de Ralph loop, por causa de um one-liner de shell que passava a um agente de código o mesmo prompt de novo e de novo. Agentes de código o usam em refactors longos, com a árvore do git e um arquivo TODO como estado.

O elenco

O mesmo elenco de sempre, desta vez num cânion.

O alçapão o seu código
Inicia um agente novo a cada volta (createIterationAgent) e recebe a resposta dele. É o loop em volta do loop.
O Astor de uma volta uma execução do agente
O loop do agente do nível 2, com o próprio bandoneón. Começa vazio e vai embora flutuando quando a volta termina.
A ponte o trabalho
O que as ferramentas mudaram no disco. Nenhuma volta joga isso fora.
A placa PROGRESS.md
Um recado curto de cada volta para a seguinte: o que está feito, o que vem depois.
DONE? isDone
A sua checagem, entre as voltas. Ela mede a ponte, não o que o modelo diz sobre ela.
LAP 3/5 maxIterations
O fusível. Se o trabalho nunca passa na checagem, o loop para mesmo assim.

Observe as duas barras no topo. Esta volta é o quanto cada requisição pesa de verdade, e ela recomeça a cada volta. 1 sessão é o quanto as mesmas requisições pesariam se um único agente tivesse feito as três voltas: ela nunca desce.

O código

Com astorlm: runGoalLoop recebe uma fábrica que devolve um agente novo, a sua checagem isDone e um fusível maxIterations. As ferramentas escrevem em arquivos, então cada volta encontra o trabalho onde a anterior o deixou.

Do zero: O loop do nível 2, chamado dentro de um for. O histórico é uma variável local de cada chamada, então cada volta começa vazia de graça.

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: '…' }

O que observar

  • Confira o trabalho, não a resposta. Um “Pronto!” do modelo não prova nada. O isDone deve olhar o resultado em si: rodar os testes, contar as linhas, medir a ponte. Mantenha-o barato e determinístico, porque ele roda depois de cada volta.
  • Sempre coloque o fusível. Uma checagem que nunca pode passar, ou um agente que vive desfazendo o próprio trabalho, fica dando voltas até a sua fatura pará-lo. maxIterations, e uma olhada no motivo de ele ter se esgotado.
  • O arquivo de progresso é a única passagem de bastão. O que ele deixar de fora, a próxima volta não sabe. Diga ao agente exatamente o que escrever ali: o que está feito, o que vem a seguir, o que ele tentou e falhou.
  • Torne cada passo seguro de repetir. Uma volta pode morrer no meio, depois do trabalho mas antes do recado. A próxima volta vai fazer aquela fatia de novo, então fazê-la duas vezes não pode quebrar nada.
  • Mantenha as fatias pequenas. Uma volta deveria caber numa execução curta. Se uma única fatia já precisa de compactação, as fatias estão grandes demais.