# Own your control flow: o loop é seu, não do modelo · Eduardo Galbiati

URL: https://www.edugalbs.com/blog/own-your-control-flow

> O fator 8 do 12-Factor Agents, o que a minha fábrica mais segue. O que ele pede, o que ele não pede e como o motor da fábrica decide cada passo, com o código e os números de 371 runs.

Tecnologia29 de outubro de 202620 min de leitura

## Own your control flow: o loop é seu, não do modelo

O fator 8 do 12-Factor Agents, o que a minha fábrica mais segue. O que ele pede, o que ele não pede e como o motor da fábrica decide cada passo, com o código e os números de 371 runs.

por Eduardo Galbiati

Fator 8 · Seja dono do seu fluxo de controle

No [guia rápido dos 12-Factor Agents](https://www.edugalbs.com/blog/12-factor-agents) eu medi quanto a minha [fábrica de software agêntica](https://www.edugalbs.com/fabrica-24x7) segue cada fator. O que deu mais alto foi o 8, _Own your control flow_: 150%. Não por acaso. Ele é o desenho original da fábrica.

Este é o primeiro post da série que aprofunda cada fator. Primeiro o conceito: o que o fator pede e o que ele não pede. Depois, como a fábrica faz, com trechos do código e números de 371 runs reais.

Os diagramas deste post são desenhados em código (SVG), não são imagens. E cada um tem, logo abaixo, a sua versão em texto, para quem lê com leitor de tela e para as IAs, que leem texto muito melhor do que figura.

### O que o fator pede

O jeito mais comum de montar um agente é um loop curto: o modelo olha a conversa, escolhe uma ferramenta, o código roda a ferramenta, o resultado volta para a conversa, e o modelo escolhe de novo. Até ele dizer que acabou.

Funciona na demo. O problema é que, nesse desenho, **o modelo é o loop**. É ele que decide o próximo passo, quando parar e se vale tentar de novo. E aí você não tem onde pôr a mão: não dá para pausar antes de algo arriscado, esperar uma pessoa, limitar as voltas ou testar o fluxo sem chamar o modelo.

O fator 8 inverte isso: **o loop é seu**. O modelo continua sugerindo o próximo passo, mas quem decide o que acontece com a sugestão é um código que você escreveu. O texto original mostra três casos, num `switch` comum:

-   o modelo pediu uma informação que o código busca na hora (as issues abertas): o código busca, junta no contexto e devolve para o modelo;
-   o modelo precisa de esclarecimento de uma pessoa: o código salva o estado, manda a pergunta e **sai do loop**; a resposta volta depois, como evento;
-   o modelo quer fazer algo de alto risco (criar algo, fazer deploy): o código salva, pede aprovação e **sai do loop** também.

Com o loop na mão, o resto vem de graça: resumir resultados de ferramenta, um LLM avaliando a saída de outro, compactar o contexto, log e métricas, limite de chamadas e, principalmente, **pausar e esperar um evento** sem um processo parado num `while ... sleep`.

Figura 1. A diferença não é usar menos IA. É quem segura o volante.

A figura em texto

**O modelo é o loop** (o jeito comum): 🤖 o modelo decide → a ferramenta roda → o resultado volta para a conversa → o modelo decide de novo. Para só quando o modelo diz que acabou. Não há onde pausar, limitar voltas ou testar sem o modelo.

**O código é o loop** (o fator 8): ⚙️ o código recebe a sugestão do modelo e escolhe um de quatro caminhos: 🤖 chamar o modelo para um passo; ⚙️ rodar um comando direto; 👤 pausar e esperar uma pessoa ou um evento; ⏹ parar. Depois de cada passo, volta para o código. Para quando o código diz.

### O que ele não é

Esse fator é fácil de ler errado. Quatro coisas que ele **não** pede:

-   **Não é tirar o julgamento do modelo.** O modelo continua decidindo o que só ele decide bem: se o código está bom, o que corrigir, como resolver um conflito. O fator pede que essa decisão volte como um dado (um campo, um veredito) e que o código escolha o caminho a partir dele.
-   **Não é um fluxograma rígido.** Um fluxo seu pode ter voltas, ramos, repetição até um objetivo e até um segundo modelo para destravar uma falha. A diferença é que cada volta foi escrita, tem nome e tem limite.
-   **Não é "não use framework".** Um framework de grafos serve, desde que o grafo seja seu e você consiga ler, mudar e testar. O que o fator condena é o loop escondido, que você não controla.
-   **Não é controlar o que acontece dentro do turno.** Dentro de um passo, o agente usa as ferramentas dele no ritmo dele. O fator fala de quem decide **entre** os passos.

### Na fábrica: o motor anda, o agente faz um passo

A fábrica tem duas peças, e os nomes valem para o post inteiro: o **motor** ⚙️ é um programa sem IA que anda pelo fluxo de trabalho; o **agente** 🤖 é a IA, chamada para fazer um passo de cada vez. O `AGENTS.md` da fábrica resume numa frase: _um motor determinístico anda pelo grafo do fluxo; um supervisor de IA só é chamado quando um passo falha, ou quando eu peço_.

São cinco práticas. Cada uma tem o seu trecho de código.

#### 1\. O grafo é declarado

Cada fluxo é um arquivo YAML com passos e transições. O fluxo de desenvolvimento, `software-development.yaml`, tem 26 passos. Os principais:

Figura 2. O caminho feliz do fluxo de desenvolvimento. Cada seta é uma linha do YAML; nenhuma é escolhida pelo modelo.

A figura em texto

1.  🤖 desenvolver (`develop`)
2.  🤖 revisar (`code_review`): se pedir mudanças, volta para o 1, no máximo 3 vezes; na quarta, uma pessoa decide (`human_review`)
3.  🤖 escrever testes (`test`)
4.  ⚙️ rodar a suíte (`suite`), sem modelo
5.  🤖 build (`build`)
6.  🤖 verificação final (`verify_change`)
7.  🤖 + ⚙️ CI verde (`ci`): o agente faz o push e abre o PR; o motor espera o CI
8.  🤖 + ⚙️ vigiar o PR (`pr_babysitter`): o motor vigia o PR e chama o agente só quando há novidade
9.  🤖 preview (`deploy_test`)
10.  👤 aprovar o merge (`approve_merge`)
11.  ⚙️ merge (`gh pr merge`, rodado pelo motor)
12.  🤖 deploy em produção (`deploy_prod`)

O revisor não decide para onde o run vai. Ele devolve um veredito num campo, e quem lê o campo é o motor:

```yaml
- id: code_review
  agent: reviewer
  output_schema:
    properties:
      verdict: { type: string, enum: [approved, changes_requested] }
  transitions:
    - when: "output.verdict == 'approved'"
      goto: test
    - when: "output.verdict == 'changes_requested'"
      goto: develop
      loop:
        name: review_cycle
        max_iterations: 3
        on_exhausted: human_review
```

É o `switch` do texto original, só que escrito como dado. O agente 🤖 julga o código; o motor ⚙️ escolhe o caminho.

#### 2\. O motor conta as voltas

Toda volta tem um limite. Quando ele estoura, o run vai para um destino declarado (uma pessoa, um passo de falha), nunca para "mais uma tentativa". Esta é a função do motor que conta, quase inteira:

```js
applyCounters(step, t) {
  let target = t.goto;
  if (t.max_visits) {
    const key = `${step.id}->${t.goto}`;
    this.counters[key] = (this.counters[key] || 0) + 1;
    if (this.counters[key] > limit(t.max_visits)) {
      target = t.on_exhausted || 'fail';
    }
  }
  if (t.loop) {
    const name = t.loop.name || `${step.id}->${t.goto}`;
    this.loops[name] = (this.loops[name] || 0) + 1;
    if (this.loops[name] > limit(t.loop.max_iterations)) {
      target = t.loop.on_exhausted || 'fail';
    }
  }
  return target;
}
```

Um detalhe importante: quando o limite estoura, o motor também **cancela a correção que estava a caminho**. Se nada mais vai conferir aquela correção, ela não deve acontecer.

Nos logs de 371 runs (de 27/09 a 24/10/2026), um limite desses parou um loop **55 vezes**. Em mais da metade, testes que continuavam falhando depois de duas correções e foram parar na mão de uma pessoa, em vez de girar a noite inteira.

#### 3\. Esperar é trabalho do motor

Esse é o ponto em que a fábrica vai além do fator. Esperar o CI é a parte mais longa de um PR, e é onde um agente com o loop na mão mais desperdiça: ele fica perguntando "já terminou?" e cada pergunta é um turno pago.

Na fábrica, o passo de CI declara um `wait:`. O agente faz o push e abre o PR; daí em diante, o motor consulta o CI sozinho, a cada minuto, com o `gh`. Ele só chama o agente se algo quebrou. E, se tudo passou, ele termina o passo sozinho, sem chamar ninguém:

```yaml
- id: ci
  agent: ci_pusher
  goal:
    until: "output.ci_status == 'success'"
    on_exhausted: ci_failed
  loop: { interval: 1m, max_duration: 1h }
  wait:
    probe: pr_checks
    target: "{{ steps.ci.output.pr_url }}"
    output: { ci_status: success, failure_log: "" }
    grace: 10m
```

Figura 3. Dois turnos de agente numa espera de mais de dez minutos. O resto é o motor olhando o CI.

A figura em texto

1.  🤖 Turno do agente: push da branch e abertura do PR.
2.  ⚙️ O motor consulta o CI a cada minuto: pendente, pendente, pendente, pendente. Nenhum modelo é chamado.
3.  ⚙️ Na quinta consulta, o CI falhou: o motor chama o agente.
4.  🤖 Turno do agente: corrige e faz push de novo.
5.  ⚙️ O motor volta a consultar: pendente, cinco vezes.
6.  ⚙️ O CI passou: o motor termina o passo sozinho, sem turno de agente.

Nos runs que esperaram o CI ou um PR, o motor fez **203 consultas**. Em 151 ainda não havia novidade e ele só esperou; em 13 o CI passou e ele fechou o passo sozinho. Só em 39 ele chamou o agente. Ou seja: **81% das consultas não custaram nenhum token**.

#### 4\. O modelo tem orçamento, não volante

Quando um passo falha, entra um segundo agente, o supervisor 🤖. Ele lê o que aconteceu e sugere uma saída: corrigir a resposta, tentar de novo com uma instrução, voltar para outro passo ou chamar uma pessoa. Mas ele também está dentro do loop do motor, com limites:

| Limite | Valor no fluxo de desenvolvimento | Quem confere |
| --- | --- | --- |
| Intervenções do supervisor por passo | 2 | ⚙️ motor |
| Intervenções do supervisor por run | 6 | ⚙️ motor |
| Gasto do supervisor por run | US$ 5 | ⚙️ motor |
| Duração do run | 8 horas | ⚙️ motor, antes de cada turno |
| Custo do run | US$ 60 | ⚙️ motor, antes de cada turno |
| Voltas da revisão | 3 | ⚙️ motor (`loop.max_iterations`) |

E a sugestão dele é conferida. Se ele quer voltar para um passo, esse passo tem que estar na lista `recover:` que o fluxo declarou para aquela falha. Uma sugestão fora da lista não é seguida; vira um pedido de ajuda para mim:

```js
const back = d.action === 'goto' && route.recover.includes(d.goto);
```

Uma linha. Mas é ela que garante que o supervisor nunca invente um caminho que eu não desenhei. Nos 371 runs, o motor tomou **4.335 decisões de transição**. Só **27** vieram de uma sugestão do supervisor.

#### 5\. A fábrica inteira roda sem IA

Se o loop é seu, você consegue testar o fluxo sem chamar o modelo. Na fábrica, `npm test` roda com `MULTIAGENT_SIMULATION_ONLY=1` e um provedor de mentira, que responde no formato de cada passo. O motor anda pelo grafo inteiro, e os testes forçam o que dá errado na vida real: respostas fora do formato, CI que falha, ferramentas recusadas, merge com conflito.

É o teste mais barato da fábrica. Mudou uma transição no YAML? Os testes mostram, em segundos, para onde cada caminho vai.

### Por que 150%

100% seria fazer o que o fator pede: um loop seu, que pausa, espera uma pessoa e para quando você quer. A fábrica faz isso e mais quatro coisas que o fator só sugere ou nem cita:

-   **espera sem modelo**: o motor vigia o CI e o PR, e chama o agente só quando há novidade;
-   **limite em toda volta**, com destino declarado quando estoura;
-   **a IA que destrava falhas também é conferida**: o supervisor só pode ir para os passos permitidos;
-   **o fluxo inteiro é testado sem IA**, a cada mudança.

### Onde o controle ainda é do modelo

Dentro de um turno. Quando o agente está trabalhando num passo, a CLI que o roda (Claude Code, Codex) tem o seu próprio loop de ferramentas, e esse loop não é meu. O que o motor faz é cercar: um tempo máximo por turno, um número máximo de chamadas, um vigia de memória e outro que percebe quando o agente parou de produzir. Acho que é o equilíbrio certo: o agente é livre dentro do passo; o caminho entre os passos é meu.

### Para levar

Se você vai montar um agente amanhã, a pergunta deste fator é uma só: **quem decide o próximo passo?** Se a resposta é "o modelo", tudo o que você quiser fazer depois (pausar, aprovar, limitar, testar) vai precisar de uma gambiarra. Se a resposta é "o meu código, a partir do que o modelo respondeu", essas coisas viram uma linha no seu fluxo.

O [guia dos 12 fatores](https://www.edugalbs.com/blog/12-factor-agents) tem a visão geral, e o [Diário da fábrica](https://www.edugalbs.com/fabrica-24x7) conta como esse motor foi construído, capítulo a capítulo. No próximo post da série, outro fator.
