DEV Community

Alair Joao Tavares
Alair Joao Tavares

Posted on Originally published at activi.dev

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

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}"
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.")
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)