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.
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:
A figura em texto
- 🤖 desenvolver (
develop) - 🤖 revisar (
code_review): se pedir mudanças, volta para o 1, no máximo 3 vezes; na quarta, uma pessoa decide (human_review) - 🤖 escrever testes (
test) - ⚙️ rodar a suíte (
suite), sem modelo - 🤖 build (
build) - 🤖 verificação final (
verify_change) - 🤖 + ⚙️ CI verde (
ci): o agente faz o push e abre o PR; o motor espera o CI - 🤖 + ⚙️ vigiar o PR (
pr_babysitter): o motor vigia o PR e chama o agente só quando há novidade - 🤖 preview (
deploy_test) - 👤 aprovar o merge (
approve_merge) - ⚙️ merge (
gh pr merge, rodado pelo motor) - 🤖 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
A figura em texto
- 🤖 Turno do agente: push da branch e abertura do PR.
- ⚙️ O motor consulta o CI a cada minuto: pendente, pendente, pendente, pendente. Nenhum modelo é chamado.
- ⚙️ Na quinta consulta, o CI falhou: o motor chama o agente.
- 🤖 Turno do agente: corrige e faz push de novo.
- ⚙️ O motor volta a consultar: pendente, cinco vezes.
- ⚙️ 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.