Eduardo Galbiati / blog

Tecnologia20 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.

Fator 8 · Seja dono do seu fluxo de controle

No guia rápido dos 12-Factor Agents eu medi quanto a minha fábrica de software agêntica 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.

Dois jeitos de montar o loop de um agente À esquerda, o modelo é o loop: decide, a ferramenta roda, o resultado volta, e ele decide de novo até dizer que acabou. À direita, o código é o loop: a cada passo ele escolhe chamar o modelo, rodar um comando direto, pausar para uma pessoa ou parar. O modelo é o loop o jeito comum: o modelo decide tudo 🤖 modelo decide ferramenta roda resultado volta para a conversa para quando o modelo diz que acabou sem pausa, sem limite de voltas, sem teste sem modelo O código é o loop o fator 8: o modelo sugere, o código decide ⚙️ código decide 🤖chama omodelo ⚙️roda umcomando 👤pausa eespera ⏹para para quando o código diz pausa, limita, espera e testa porque cada saída passa por ele Dois jeitos de montar o loop de um agente Em cima, o modelo é o loop: decide, a ferramenta roda, o resultado volta, e ele decide de novo até dizer que acabou. Embaixo, o código é o loop: a cada passo ele escolhe chamar o modelo, rodar um comando direto, pausar para uma pessoa ou parar. O modelo é o loop o jeito comum: o modelo decide tudo 🤖 modelo decide ferramenta roda resultado voltapara a conversa para quando o modelo diz que acabou sem pausa, sem limite de voltas, sem teste sem modelo O código é o loop o fator 8: o modelo sugere, o código decide ⚙️ código decide 🤖chama omodelo ⚙️roda umcomando 👤pausa eespera ⏹para para quando o código diz pausa, limita, espera e testa porque cada saída passa por ele
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:

O fluxo de desenvolvimento da fábrica, passo a passo Doze passos em três linhas: desenvolver, revisar, escrever testes, rodar a suíte, build, verificação final, CI verde, acompanhar o PR, preview, aprovar o merge, merge e deploy em produção. A revisão pode devolver para o desenvolvimento até três vezes; depois disso, uma pessoa decide. pede mudanças · até 3 voltas, depois 👤 🤖 desenvolverdevelop 🤖 revisarcode_review 🤖 escrever testestest ⚙️ rodar a suítesuite 🤖 buildbuild 🤖 verificação finalverify_change 🤖 + ⚙️ CI verdeci 🤖 + ⚙️ vigiar o PRpr_babysitter 🤖 previewdeploy_test 👤 aprovar o mergeapprove_merge ⚙️ mergegh pr merge 🤖 produçãodeploy_prod 🤖 um agente faz o passo · ⚙️ o motor faz sozinho · 🤖 + ⚙️ o agente age, o motor espera · 👤 uma pessoa decide O fluxo de desenvolvimento da fábrica, passo a passo Doze passos, um embaixo do outro: desenvolver, revisar, escrever testes, rodar a suíte, build, verificação final, CI verde, vigiar o PR, preview, aprovar o merge, merge e deploy em produção. A revisão pode devolver para o desenvolvimento até três vezes; depois disso, uma pessoa decide. pede mudanças: até 3 voltas, depois 👤 🤖 desenvolverdevelop🤖 revisarcode_review🤖 escrever testestest⚙️ rodar a suítesuite🤖 buildbuild🤖 verificação finalverify_change🤖 + ⚙️ CI verdeci🤖 + ⚙️ vigiar o PRpr_babysitter🤖 previewdeploy_test👤 aprovar o mergeapprove_merge⚙️ mergegh pr merge🤖 produçãodeploy_prod 🤖 um agente faz o passo⚙️ o motor faz sozinho🤖 + ⚙️ o agente age, o motor espera👤 uma pessoa decide
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:

- 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:

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:

- 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
Uma espera de CI na fábrica Linha do tempo de um passo de CI. O agente faz o push e abre o PR. O motor consulta o CI a cada minuto, sem modelo. Na quinta consulta o CI falha e o motor chama o agente para corrigir. Mais consultas do motor, sem modelo. O CI passa e o motor termina o passo sozinho. 🤖 turnos do agente (custam modelo) ⚙️ consultas do motor (não custam) push + PR corrige pendente, pendente… falhou: chama o agente pendente, pendente… passou: o motor fecha ilustração: uma bolinha por minuto; o desenho é um exemplo, os números reais estão no texto Uma espera de CI na fábrica Linha do tempo de um passo de CI, de cima para baixo. O agente faz o push e abre o PR. O motor consulta o CI a cada minuto, sem modelo. Na quinta consulta o CI falha e o motor chama o agente para corrigir. Mais consultas do motor, sem modelo. O CI passa e o motor termina o passo sozinho. 🤖 agente⚙️ motor (não custa) push + PR pendente ×4 falhou: chama o agente corrige pendente ×5 passou: o motor fecha ilustração: uma bolinha por minuto; os números reais estão no texto
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:

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 tem a visão geral, e o Diário da fábrica conta como esse motor foi construído, capítulo a capítulo. No próximo post da série, outro fator.