Arquitetando AI Agents de Alta Velocidade: Lições do Release 0.30.0

Tem dias em que tudo flui. O contexto está fresco, as decisões são rápidas, o código sai limpo. E tem dias em que você está tentando fazer um AI agent verificar uma interface no browser, o loop de feedback está quebrado, e você percebe que o problema não é o modelo — é a arquitetura ao redor dele. O release 0.30.0 do meu projeto de developer tools foi um divisor de águas exatamente porque forcei um encontro com esse segundo tipo de dia. Neste artigo, quero compartilhar o que aprendi sobre construir feedback loops confiáveis em agentes de código com Claude Code, especialmente no contexto de verificação browser-based e auditorias de design automatizadas.

O Problema: Agents Sem Chão Firme

A promessa dos AI coding agents é poderosa: você descreve uma tarefa, o agente executa uma sequência de ações, verifica o resultado e itera. Na prática, o ponto mais frágil dessa cadeia é justamente a verificação. Quando o agente escreve código backend, verificar o output é relativamente direto — você roda os testes, lê o stdout, compara o resultado com um schema esperado. Mas quando o domínio é frontend, a coisa complica.

No meu caso, eu queria que o agente conseguisse:

  1. Implementar uma mudança de UI (reorganizar componentes, ajustar espaçamentos, padronizar labels)
  2. Renderizar a aplicação em um browser real
  3. Capturar evidências visuais do resultado
  4. Comparar com um critério de aceitação de design
  5. Iterar se necessário

O loop parece simples. Mas sem uma estrutura cuidadosa, o agente entra em colapso silencioso: ele acredita que completou a tarefa porque não recebeu nenhum erro — mas ninguém verificou de fato o que foi renderizado.

Isso me ensinou a primeira grande lição: um agente sem feedback explícito e estruturado é um agente que alucina completude.

Construindo o Loop de Feedback: A Arquitetura que Funcionou

Depois de algumas iterações frustradas, cheguei a uma arquitetura em três camadas para o feedback loop do agente. Vou descrever em Python, que é onde faço a orquestração, com TypeScript aparecendo na camada de verificação browser.

Camada 1: O Orquestrador em Python

O orquestrador é responsável por definir a tarefa, invocar o agente, e processar o resultado de cada ciclo. Uso uma estrutura simples de dataclass para representar o estado do ciclo:

from dataclasses import dataclass, field from typing import Optional import subprocess import json @dataclass class AgentCycle: task_description: str acceptance_criteria: list[str] max_iterations: int = 5 current_iteration: int = 0 verification_results: list[dict] = field(default_factory=list) completed: bool = False def run_agent_cycle(cycle: AgentCycle) -> AgentCycle: while not cycle.completed and cycle.current_iteration < cycle.max_iterations: cycle.current_iteration += 1 print(f"[Iteration {cycle.current_iteration}] Running agent...") # Invoca o agente (Claude Code via CLI ou SDK)  agent_result = invoke_agent( task=cycle.task_description, context=build_context(cycle) ) # Roda verificação após cada ação do agente  verification = verify_output(cycle.acceptance_criteria) cycle.verification_results.append(verification) if verification["passed"]: cycle.completed = True print(f"[Iteration {cycle.current_iteration}] ✓ All criteria met.") else: failed = [c for c in verification["criteria"] if not c["passed"]] print(f"[Iteration {cycle.current_iteration}] ✗ {len(failed)} criteria failed. Continuing...") # Atualiza a task com o contexto do que falhou  cycle.task_description = enrich_task_with_failures( cycle.task_description, failed ) return cycle def enrich_task_with_failures(original_task: str, failed_criteria: list[dict]) -> str: failure_summary = "\n".join( f"- AINDA PENDENTE: {c['description']} (encontrado: {c['actual']}, esperado: {c['expected']})" for c in failed_criteria ) return f"{original_task}\n\nCritérios ainda não atendidos:\n{failure_summary}" 

O ponto crítico aqui é o enrich_task_with_failures. Em vez de simplesmente re-executar a mesma tarefa, eu realimento o agente com evidências concretas do que ainda está errado. Isso evita que ele fique em loop tentando a mesma abordagem.

Camada 2: Verificação Browser com TypeScript

Para a verificação visual, uso um script TypeScript com Playwright que captura screenshots e extrai métricas de UI:

import { chromium, Browser, Page } from 'playwright'; import * as fs from 'fs'; interface VerificationCriteria { selector: string; property: string; expected: string; description: string; } interface CriterionResult { description: string; passed: boolean; actual: string; expected: string; } interface VerificationReport { passed: boolean; screenshotPath: string; criteria: CriterionResult[]; timestamp: string; } async function runBrowserVerification( url: string, criteria: VerificationCriteria[] ): Promise<VerificationReport> { const browser: Browser = await chromium.launch(); const page: Page = await browser.newPage(); await page.goto(url, { waitUntil: 'networkidle' }); const results: CriterionResult[] = []; for (const criterion of criteria) { const element = page.locator(criterion.selector); let actual = ''; try { if (criterion.property === 'text') { actual = await element.innerText(); } else if (criterion.property === 'visible') { actual = String(await element.isVisible()); } else { // CSS properties actual = await element.evaluate( (el, prop) => getComputedStyle(el).getPropertyValue(prop), criterion.property ); } results.push({ description: criterion.description, passed: actual.trim() === criterion.expected.trim(), actual: actual.trim(), expected: criterion.expected.trim(), }); } catch (err) { results.push({ description: criterion.description, passed: false, actual: `ERROR: ${err}`, expected: criterion.expected, }); } } const screenshotPath = `verification-${Date.now()}.png`; await page.screenshot({ path: screenshotPath, fullPage: true }); await browser.close(); return { passed: results.every(r => r.passed), screenshotPath, criteria: results, timestamp: new Date().toISOString(), }; } // Entry point: lê critérios do stdin, escreve report no stdout async function main() { const input = JSON.parse(process.argv[2]); const report = await runBrowserVerification(input.url, input.criteria); console.log(JSON.stringify(report, null, 2)); } main().catch(console.error); 

Esse script é invocado pelo orquestrador Python via subprocess, e o report JSON retornado alimenta diretamente a função verify_output. O screenshot vai para um diretório de evidências que eu posso consultar quando algo der errado.

Camada 3: Auditoria de Design como Critério Estruturado

Uma das partes mais valiosas desse release foi formalizar o que chamo de "design audit criteria" — critérios de aceitação que vão além de "o componente existe" e chegam em "o componente se comporta conforme o design system".

from typing import TypedDict class DesignCriterion(TypedDict): selector: str property: str expected: str description: str def build_design_audit_criteria(audit_spec: dict) -> list[DesignCriterion]: """ Converte uma spec de auditoria de design em critérios verificáveis. Exemplo de audit_spec: { "typography": { "page-title": {"font-size": "24px", "font-weight": "600"}, }, "labels": { "pipeline-stage-label": {"text": "Opportunity"} }, "visibility": { "billing-status-indicator": {"visible": "true"} } } """ criteria: list[DesignCriterion] = [] for category, elements in audit_spec.items(): for selector, properties in elements.items(): for prop, expected in properties.items(): criteria.append(DesignCriterion( selector=f"[data-testid='{selector}']", property=prop, expected=str(expected), description=f"[{category}] {selector}: {prop} deve ser '{expected}'" )) return criteria # Exemplo de uso audit_spec = { "labels": { "pipeline-stage-label": {"text": "Opportunity"}, "billing-log-status": {"text": "Paid"}, }, "typography": { "section-header": {"font-size": "18px"}, }, "visibility": { "dispute-indicator": {"visible": "true"}, } } criteria = build_design_audit_criteria(audit_spec) print(f"Auditoria com {len(criteria)} critérios gerada.") 

Essa estrutura me deu algo que eu não tinha antes: uma linguagem comum entre o que eu quero que o agente faça e como eu verifico que ele fez. Sem isso, a auditoria é subjetiva e o agente não tem como saber se acertou.

O Que Aprendi Sobre Claude Code nesse Contexto

Trabalhar com Claude Code como o "cérebro" do agente me ensinou algumas coisas específicas sobre como estruturar as instruções.

Seja cirúrgico no contexto. Quando o agente precisa fazer uma mudança de UI — como padronizar um label de vocabulário em vários componentes — quanto mais específico for o contexto fornecido, melhor. Em vez de dizer "padronize os labels de pipeline", funciona melhor dizer: "os componentes PipelineCard, StageFilter e OpportunityBadge usam a string 'lead' em alguns lugares e 'opportunity' em outros. Padronize para 'opportunity' em todos eles."

Feedback de falha é mais valioso que confirmação de sucesso. Quando o verificador browser retorna que um critério falhou, incluir o valor atual junto com o valor esperado no prompt do próximo ciclo corta o número de iterações necessárias pela metade, na minha experiência.

Separe geração de verificação. Uma armadilha clássica é pedir que o agente verifique o próprio trabalho. Isso é como pedir ao programador que faça o próprio code review logo depois de escrever o código — o viés de confirmação entra em ação. A verificação precisa ser uma camada independente, idealmente determinística (testes, screenshots, métricas extraídas do DOM).

Dicas Práticas para Quem Está Construindo Algo Parecido

  • Comece com critérios simples e expansão progressiva. No primeiro ciclo, verifique apenas se a aplicação carrega sem erros de console. Adicione critérios de design e comportamento nas iterações seguintes. Agentes que tentam satisfazer 20 critérios de uma vez tendem a falhar em todos eles.

  • Use data-testid como contrato. Se o seu frontend usa data-testid de forma consistente, o verificador browser tem um target estável mesmo quando o layout muda. Convenci o agente a adicionar esses atributos como parte da tarefa de implementação, não como afterthought.

  • Versione os reports de verificação. Cada execução do loop gera um JSON de verificação e um screenshot. Manter esses artefatos me salvou várias vezes quando precisei entender por que um ciclo específico falhou.

  • Defina um budget de iterações e seja honesto sobre falha. Se o agente não satisfez os critérios em N iterações, não é útil continuar em loop. Marque como falha, logue os critérios pendentes, e escale para revisão humana. Agentes que nunca falham explicitamente são agentes que escondem problemas.

  • Teste sua pipeline de verificação sem o agente primeiro. Antes de integrar Claude Code, rode o verificador browser manualmente com critérios que você sabe que passam e critérios que você sabe que falham. Se o verificador não é confiável, o agente vai otimizar para uma métrica errada.

Conclusão

O release 0.30.0 foi significativo não pelo número de features que shipper, mas pela mudança de mentalidade que forçou. Eu parei de pensar em AI agents como "ferramentas que fazem coisas" e comecei a pensar neles como processos que precisam de observabilidade. O loop de feedback não é um detalhe de implementação — é a arquitetura central.

Verificação browser-based ainda é difícil. Flakiness, timing issues, estados de UI que dependem de autenticação — há muitos problemas não resolvidos. Mas a estrutura de três camadas (orquestrador Python, verificador TypeScript/Playwright, critérios de design estruturados) me deu uma base sólida para iterar.

Se você está construindo agentes de código e ainda não tem uma camada de verificação independente, esse é o investimento mais valioso que pode fazer. O modelo é bom. O que frequentemente falha é a infraestrutura ao redor dele.