> Nível 2 de Agent Harness Patterns, uma trilha de padrões sobre como funcionam os agentes de IA. Versão web: https://harnesspatterns.dev/pt/patterns/agent-loop · Todos os padrões (em inglês): https://harnesspatterns.dev/llms.txt

# O loop do agente

Sozinho, um modelo não consegue fazer nada: ele só gera texto. O loop do agente é o que o transforma num agente. Ele passa o histórico para o modelo, executa as ferramentas que o modelo pede, devolve os resultados e repete até o modelo dizer "pronto".

## O problema

Uma mamãe pássaro pergunta a um modelo "você consegue limpar o forte dos porcos?". O modelo não consegue lançar nada. O máximo que ele pode fazer é responder com um *pedido* de ferramenta: `{ name: "launch_red", input: { angle: 40 } }`.

Se o seu código faz uma única chamada ao modelo, a conversa termina ali. Você fica com um pedido que ninguém executou e sem resposta. Se você executa a ferramenta na mão, bate no mesmo problema na rodada seguinte, porque o modelo pode precisar de outra ferramenta, e depois de mais outra.

## A solução

Um loop com uma única regra de saída:

1. Envie ao modelo **o histórico inteiro** mais a lista de ferramentas disponíveis.
2. Se a resposta terminar com `stopReason: "end_turn"`, devolva-a. Essa é a única saída normal.
3. Se terminar com `"tool_use"`, execute cada ferramenta pedida, acrescente os resultados ao histórico como blocos `tool_result` e volte ao passo 1.

O modelo decide *o que* fazer; o loop é quem *faz*. Essa divisão é a base de todos os outros padrões: todo o resto (steering, compactação de contexto, subagentes) se encaixa em algum ponto deste loop.

## O elenco

O loop contado como uma pequena aventura. Depois que você conhece o elenco, não sobra nada para decifrar.

- **Astor** (o loop): Um pequeno tanguero, e o único que se mexe. Leva a pergunta ao Oráculo, corre até o estilingue a cada tiro e traz a resposta de volta para a mamãe pássaro.
- **O Oráculo** (Provider): O modelo. Nunca toca no estilingue: só escuta o bandoneón e devolve um bilhete. Laranja se precisa de uma ferramenta, dourado se terminou.
- **O bandoneón** (messages[]): O histórico, uma dobra colorida por mensagem. O fole cresce a cada volta, e o Oráculo escuta cada dobra toda vez. São as notas que sobem flutuando até o Oráculo.
- **O banco** (ToolRegistry): Um pássaro por ferramenta: `launch_red`, `launch_bomb` e um terceiro de que ninguém precisa hoje. Astor lança o que o bilhete indica e mostra o resultado: verde se funcionou.
- **A mamãe pássaro** (agent.run()): O seu código. Faz a pergunta e espera.
- **Rastros, pontuação e pássaros** (histórico, tokens, maxTurns): Cada tiro deixa seu rastro no céu, do mesmo jeito que o histórico guarda cada resultado. A pontuação são os tokens, e ela pula mais a cada volta porque o histórico inteiro é enviado de novo. Cada volta custa um pássaro da fileira da barra superior, o orçamento de turnos. Os números são ilustrativos.

O painel EventBus mostra os eventos que o loop real emite em cada passo da animação.

## O código

**Com astorlm:** O mesmo loop vive em `src/agent/loop.ts`, com streaming, novas tentativas, hooks, execução de ferramentas em paralelo e cancelamento. Visto de fora, ele fica assim.

**Do zero:** Umas 40 linhas contra qualquer endpoint compatível com a OpenAI, sem SDK: `fetch` puro em TypeScript, a biblioteca padrão em Python. Os três passos acima estão marcados nos comentários. Preencha o bloco `LLM` do início com o seu próprio endpoint, modelo e chave.

**Com astorlm**

```ts
import { OpenAIProvider, createLocalAgent, tool } from 'astorlm'
import { z } from 'zod'

// Your game's functions, wrapped as tools: one per bird.
const angle = z.number().min(10).max(80).describe('Launch angle in degrees')

const launchRed = tool({
  name: 'launch_red',
  description: 'Fling the red bird. Good against wood. Returns what fell and how many pigs are left.',
  schema: z.object({ angle }),
  execute: async ({ angle }) => level.fling('red', angle), // your code
})

const launchBomb = tool({
  name: 'launch_bomb',
  description: 'Fling the bomb bird. It explodes on impact: the one to use against stone.',
  schema: z.object({ angle }),
  execute: async ({ angle }) => level.fling('bomb', angle), // your code
})

const agent = await createLocalAgent({
  // Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
  provider: new OpenAIProvider({
    baseURL: 'http://localhost:11434/v1', // e.g. Ollama's default address
    model: 'your-model', // e.g. 'llama3.1', 'gpt-4o-mini'
    apiKey: 'YOUR_API_KEY', // local servers usually ignore it
  }),
  tools: [launchRed, launchBomb],
  maxTurns: 10, // the birds in line: a cap on the laps
})

agent.on('tool-start', (tool) => console.log('→', tool.name, tool.input))

const answer = await agent.run('The pigs took our eggs! Can you clear their fort?')
```

**TypeScript**

```ts
// Agent loop from scratch. Plain fetch, no SDK.

// Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
const LLM = {
  baseURL: 'http://localhost:11434/v1', // e.g. Ollama's default address
  model: 'your-model', // e.g. 'llama3.1', 'gpt-4o-mini'
  apiKey: 'YOUR_API_KEY', // local servers usually ignore it
}

type ToolCall = { id: string; function: { name: string; arguments: string } }
type Message =
  | { role: 'user'; content: string }
  | { role: 'assistant'; content: string | null; tool_calls?: ToolCall[] }
  | { role: 'tool'; tool_call_id: string; content: string }

type ToolFn = (args: Record<string, string>) => Promise<string>
const tools: Record<string, ToolFn> = { launch_red: launchRed, launch_bomb: launchBomb }
const toolSchemas = [/* one JSON Schema per tool */]

export async function runAgent(prompt: string, maxTurns = 10): Promise<string> {
  const messages: Message[] = [{ role: 'user', content: prompt }]

  for (let turn = 1; turn <= maxTurns; turn++) {
    // 1. Send the whole history plus the tool list.
    const res = await fetch(`${LLM.baseURL}/chat/completions`, {
      method: 'POST',
      headers: { 'content-type': 'application/json', authorization: `Bearer ${LLM.apiKey}` },
      body: JSON.stringify({ model: LLM.model, messages, tools: toolSchemas }),
    })
    const [choice] = (await res.json()).choices
    const reply: Message = choice.message
    messages.push(reply)

    // 2. No tool calls: the model is done. The only normal exit.
    if (choice.finish_reason !== 'tool_calls') return reply.content ?? ''

    // 3. Run each requested tool and feed the result back as a message.
    for (const call of reply.tool_calls ?? []) {
      const run = tools[call.function.name]
      let output = `Unknown tool: ${call.function.name}`
      if (run) {
        try {
          output = await run(JSON.parse(call.function.arguments))
        } catch (err) {
          output = `Error: ${err instanceof Error ? err.message : err}`
        }
      }
      messages.push({ role: 'tool', tool_call_id: call.id, content: output })
    }
  }
  throw new Error(`No answer after ${maxTurns} turns`)
}
```

**Python**

```python
# Agent loop from scratch. Standard library only, no SDK.
import json
import urllib.request

# Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy...
LLM = {
    "base_url": "http://localhost:11434/v1",  # e.g. Ollama's default address
    "model": "your-model",  # e.g. "llama3.1", "gpt-4o-mini"
    "api_key": "YOUR_API_KEY",  # local servers usually ignore it
}

TOOLS = {"launch_red": launch_red, "launch_bomb": launch_bomb}
TOOL_SCHEMAS = [...]  # one JSON Schema per tool

def chat(messages):
    request = urllib.request.Request(
        f"{LLM['base_url']}/chat/completions",
        data=json.dumps({"model": LLM["model"], "messages": messages, "tools": TOOL_SCHEMAS}).encode(),
        headers={"Content-Type": "application/json", "Authorization": f"Bearer {LLM['api_key']}"},
    )
    with urllib.request.urlopen(request) as response:
        return json.load(response)["choices"][0]

def run_agent(prompt, max_turns=10):
    messages = [{"role": "user", "content": prompt}]

    for _ in range(max_turns):
        # 1. Send the whole history plus the tool list.
        choice = chat(messages)
        reply = choice["message"]
        messages.append(reply)

        # 2. No tool calls: the model is done. The only normal exit.
        if choice["finish_reason"] != "tool_calls":
            return reply.get("content") or ""

        # 3. Run each requested tool and feed the result back as a message.
        for call in reply.get("tool_calls", []):
            name = call["function"]["name"]
            run = TOOLS.get(name)
            try:
                output = run(**json.loads(call["function"]["arguments"])) if run else f"Unknown tool: {name}"
            except Exception as err:
                output = f"Error: {err}"
            messages.append({"role": "tool", "tool_call_id": call["id"], "content": output})

    raise RuntimeError(f"No answer after {max_turns} turns")
```

Repare que um erro de ferramenta não para o loop: ele volta para o modelo como texto, para que ele possa se corrigir na rodada seguinte.

## Quando usar, e o que observar

**Sempre que o modelo precisar agir** (ler, buscar, executar algo) antes de poder responder. Se você só precisa transformar texto, uma única chamada basta, e sai mais barato.

- **O custo cresce a cada volta.** Observe o bandoneón e o contador de tokens: o Oráculo escuta cada dobra a cada turno, então cada volta reenvia tudo o que veio antes. Com turnos suficientes, o contexto enche.
- **Sempre defina `maxTurns`.** Um modelo confuso pode continuar pedindo ferramentas para sempre. Sem um limite, o loop também roda para sempre.

> Falha real
>
> Durante uma execução de vários turnos, o proxy fez failover para um modelo gratuito mais fraco. O modelo caiu num loop de raciocínio sem sentido e nunca devolveu `end_turn`. Quem o parou foi um timeout de 60 segundos, não o loop. `maxTurns` protege você de turnos demais, mas não de um turno que nunca termina: para isso você precisa de um timeout ou de um watchdog (algo que corte a execução quando não há progresso).

## Padrões relacionados

- [4 · Quando parar](https://harnesspatterns.dev/pt/patterns/when-to-stop.md)
- [5 · Erros no loop](https://harnesspatterns.dev/pt/patterns/errors-in-the-loop.md)
- [6 · Hooks](https://harnesspatterns.dev/pt/patterns/hooks.md)
- [10 · Voltas limpas](https://harnesspatterns.dev/pt/patterns/fresh-laps.md)
