Observabilidade, Resiliência e Gestão de Estado em Redes Multi-Agente com Antigravity

Diagrama técnico mostrando o fluxo de mensagens entre agentes, fila principal com DLX, validador Pydantic e armazenamento de estado persistente.

O Problema Prático e Contexto Direto

Sistemas multi-agente distribuídos operam de forma assíncrona, o que introduz desafios complexos de consistência de estado, rastreabilidade e tolerância a falhas. Quando múltiplos agentes colaboram para resolver uma tarefa, a perda de uma única mensagem ou a corrupção do estado de um sub-agente pode paralisar todo o fluxo de trabalho. Sem um mecanismo de persistência robusto e rastreamento ponta a ponta, diagnosticar gargalos ou falhas de execução em produção torna-se inviável.

Para mitigar esses problemas, este tutorial demonstra como estruturar uma arquitetura de mensageria resiliente integrada ao framework Antigravity. Utilizaremos o RabbitMQ como barramento de mensagens confiável — configurado com confirmações de publicação (Publisher Confirms), filas duráveis e tratamento de mensagens venenosas (Poison Messages) via Dead Letter Exchange (DLX) — combinado com validação rigorosa de esquema via Pydantic e rastreamento de estado unificado. Esta abordagem complementa os conceitos apresentados no artigo principal Arquitetura de Agentes e Sub-agentes com Antigravity e Gemini 3.8: Guia Definitivo de Sistemas Autônomos em Produção.

Pré-requisitos e Instalação

Para executar este tutorial, você precisará de um ambiente Python 3.10+ e de uma instância do RabbitMQ em execução.

Instale as dependências necessárias via terminal:

pip install pika pydantic

Para subir uma instância local do RabbitMQ com o painel de gerenciamento habilitado via Docker, execute:

docker run -d --name rabbitmq-antigravity \
  -p 5672:5672 -p 15672:15672 \
  rabbitmq:3-management

Implementação Completa em Python

O script abaixo implementa o ciclo completo de vida de uma mensagem de estado entre agentes. Ele configura as filas com propriedades de resiliência, valida a carga útil usando Pydantic, simula o processamento de um agente e gerencia falhas de forma determinística.

import json
import logging
import sys
import uuid
from typing import Dict, Any, Optional
import pika
from pika.exceptions import AMQPChannelError, AMQPConnectionError
from pydantic import BaseModel, Field, ValidationError

# Configuração de Logging para Observabilidade
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s - %(message)s",
    handlers=[logging.StreamHandler(sys.stdout)]
)
logger = logging.getLogger("AntigravityResilience")

# Definição do Esquema de Estado com Pydantic
class AgentState(BaseModel):
    task_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
    parent_agent: str
    current_agent: str
    payload: Dict[str, Any]
    step_count: int = Field(default=1, ge=1)
    correlation_id: str

    class Config:
        frozen = True

class StateManager:
    """Gerencia a conexão, declaração de filas e publicação de mensagens."""

    def __init__(self, amqp_url: str = "amqp://guest:guest@localhost:5672/"):
        self.amqp_url = amqp_url
        self.connection: Optional[pika.BlockingConnection] = None
        self.channel: Optional[pika.adapters.blocking_connection.BlockingChannel] = None

    def connect(self) -> None:
        try:
            parameters = pika.URLParameters(self.amqp_url)
            self.connection = pika.BlockingConnection(parameters)
            self.channel = self.connection.channel()
            # Habilita confirmações de entrega do publicador (Publisher Confirms)
            self.channel.confirm_delivery()
            logger.info("Conexão estabelecida com o RabbitMQ.")
        except AMQPConnectionError as err:
            logger.error(f"Falha ao conectar ao RabbitMQ: {err}")
            raise

    def setup_topology(self) -> None:
        """Declara exchanges, filas duráveis e bindings para resiliência."""
        if not self.channel:
            raise RuntimeError("Canal não inicializado. Chame connect() primeiro.")

        try:
            # 1. Configuração da infraestrutura de Dead Letter (DLX)
            self.channel.exchange_declare(
                exchange="dlx.antigravity",
                exchange_type="direct",
                durable=True
            )
            self.channel.queue_declare(
                queue="q.antigravity.dlq",
                durable=True
            )
            self.channel.queue_bind(
                exchange="dlx.antigravity",
                queue="q.antigravity.dlq",
                routing_key="dead_letter"
            )

            # 2. Configuração da infraestrutura Principal
            self.channel.exchange_declare(
                exchange="ex.antigravity.work",
                exchange_type="direct",
                durable=True
            )

            # Fila principal configurada para encaminhar falhas para a DLX
            arguments = {
                "x-dead-letter-exchange": "dlx.antigravity",
                "x-dead-letter-routing-key": "dead_letter"
            }
            self.channel.queue_declare(
                queue="q.antigravity.state",
                durable=True,
                arguments=arguments
            )
            self.channel.queue_bind(
                exchange="ex.antigravity.work",
                queue="q.antigravity.state",
                routing_key="agent_state"
            )
            logger.info("Topologia de mensageria declarada com sucesso.")
        except AMQPChannelError as err:
            logger.error(f"Erro ao declarar topologia: {err}")
            raise

    def publish_state(self, state: AgentState) -> bool:
        """Publica o estado do agente garantindo a entrega via Publisher Confirms."""
        if not self.channel:
            raise RuntimeError("Canal não inicializado.")

        try:
            body = state.json()
            properties = pika.BasicProperties(
                delivery_mode=2,  # Torna a mensagem persistente no disco
                content_type="application/json",
                correlation_id=state.correlation_id
            )

            self.channel.basic_publish(
                exchange="ex.antigravity.work",
                routing_key="agent_state",
                body=body,
                properties=properties
            )
            logger.info(f"Estado publicado com sucesso. Correlation ID: {state.correlation_id}")
            return True
        except pika.exceptions.UnroutableError:
            logger.error("Mensagem não pôde ser roteada.")
            return False
        except Exception as err:
            logger.error(f"Erro na publicação: {err}")
            return False

    def close(self) -> None:
        if self.connection and not self.connection.is_closed:
            self.connection.close()
            logger.info("Conexão com o RabbitMQ encerrada.")

def process_message(ch: Any, method: Any, properties: Any, body: bytes) -> None:
    """Callback de consumo que valida o estado e simula o processamento do agente."""
    try:
        # Parse e validação estrita com Pydantic
        data = json.loads(body.decode("utf-8"))
        state = AgentState(**data)

        logger.info(
            f"Processando tarefa {state.task_id} no agente '{state.current_agent}'. "
            f"Passo: {state.step_count}. Correlation ID: {properties.correlation_id}"
        )

        # Simulação de regra de negócio: rejeita payloads inválidos para simular DLQ
        if state.payload.get("status") == "invalid":
            raise ValueError("Payload corrompido ou inválido detectado.")

        # Confirmação manual de processamento bem-sucedido
        ch.basic_ack(delivery_tag=method.delivery_tag)
        logger.info(f"Mensagem {method.delivery_tag} confirmada (ACK).")

    except (ValidationError, json.JSONDecodeError) as err:
        logger.error(f"Erro de validação de esquema. Enviando para DLQ. Erro: {err}")
        # nack com requeue=False envia a mensagem diretamente para a DLX configurada
        ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)

    except Exception as err:
        logger.error(f"Erro operacional durante o processamento: {err}. Descartando mensagem.")
        ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)

def start_consumer(amqp_url: str = "amqp://guest:guest@localhost:5672/") -> None:
    """Inicia o loop de consumo de mensagens."""
    parameters = pika.URLParameters(amqp_url)
    connection = pika.BlockingConnection(parameters)
    channel = connection.channel()

    # Garante que o agente receba apenas uma mensagem por vez (Fair Dispatch)
    channel.basic_qos(prefetch_count=1)

    channel.basic_consume(
        queue="q.antigravity.state",
        on_message_callback=process_message,
        auto_ack=False
    )

    logger.info("Aguardando novas mensagens de estado. Para sair pressione CTRL+C")
    try:
        channel.start_consuming()
    except KeyboardInterrupt:
        channel.stop_consuming()
        connection.close()
        logger.info("Consumidor finalizado.")

Como Testar e Verificar a Saída

Para validar o fluxo de resiliência e a gestão de estado, execute o script em dois cenários distintos: processamento bem-sucedido e falha com desvio para a Dead Letter Queue (DLQ).

Cenário 1: Fluxo Nominal (Sucesso)

Crie um arquivo de execução chamado run_pipeline.py para instanciar o publicador e enviar um estado válido:

from main import StateManager, AgentState, start_consumer
import threading
import time

# Inicializa o gerenciador e configura a topologia
manager = StateManager()
manager.connect()
manager.setup_topology()

# Cria um estado válido
valid_state = AgentState(
    parent_agent="Orchestrator",
    current_agent="DataAnalyzer",
    payload={"status": "active", "data": [10, 20, 30]},
    correlation_id="corr-9999-valid"
)

# Publica a mensagem
manager.publish_state(valid_state)
manager.close()

# Inicia o consumidor em uma thread separada para processar a mensagem
consumer_thread = threading.Thread(target=start_consumer, daemon=True)
consumer_thread.start()

# Aguarda o processamento
time.sleep(2)

Saída esperada no console:

2023-10-27 10:00:00,102 [INFO] AntigravityResilience - Conexão estabelecida com o RabbitMQ.
2023-10-27 10:00:00,115 [INFO] AntigravityResilience - Topologia de mensageria declarada com sucesso.
2023-10-27 10:00:00,125 [INFO] AntigravityResilience - Estado publicado com sucesso. Correlation ID: corr-9999-valid
2023-10-27 10:00:00,130 [INFO] AntigravityResilience - Conexão com o RabbitMQ encerrada.
2023-10-27 10:00:00,135 [INFO] AntigravityResilience - Aguardando novas mensagens de estado. Para sair pressione CTRL+C
2023-10-27 10:00:00,142 [INFO] AntigravityResilience - Processando tarefa 550e8400-e29b-41d4-a716-446655440000 no agente 'DataAnalyzer'. Passo: 1. Correlation ID: corr-9999-valid
2023-10-27 10:00:00,145 [INFO] AntigravityResilience - Mensagem 1 confirmada (ACK).

Cenário 2: Fluxo de Falha (Encaminhamento para DLQ)

Modifique o payload para simular um erro de validação ou uma mensagem corrompida:

manager = StateManager()
manager.connect()

invalid_state = AgentState(
    parent_agent="Orchestrator",
    current_agent="DataAnalyzer",
    payload={"status": "invalid"},  # Forçará o disparo da exceção
    correlation_id="corr-9999-invalid"
)

manager.publish_state(invalid_state)
manager.close()

Saída esperada no console:

2023-10-27 10:05:00,010 [INFO] AntigravityResilience - Estado publicado com sucesso. Correlation ID: corr-9999-invalid
2023-10-27 10:05:00,022 [INFO] AntigravityResilience - Processando tarefa 6a2b3c4d-e89b-12d3-a456-426655440000 no agente 'DataAnalyzer'. Passo: 1. Correlation ID: corr-9999-invalid
2023-10-27 10:05:00,025 [ERROR] AntigravityResilience - Erro operacional durante o processamento: Payload corrompido ou inválido detectado.. Descartando mensagem.

Ao inspecionar o painel do RabbitMQ (http://localhost:15672), você observará que a mensagem rejeitada foi movida automaticamente para a fila q.antigravity.dlq, preservando o payload original e os metadados para análise forense.

Armadilhas Operacionais e Boas Práticas

1. Reentrância e Idempotência

Agentes distribuídos podem receber a mesma mensagem mais de uma vez devido a timeouts de rede ou reinicializações de pods. Garanta que toda operação de escrita de estado seja idempotente. Utilize o correlation_id ou o task_id como chave primária única no banco de dados de estado para evitar duplicidade de processamento.

2. Loops de Mensagens Venenosas (Poison Messages)

Se uma mensagem falhar continuamente e for devolvida à fila principal (requeue=True), ela consumirá recursos infinitamente. A configuração apresentada de nack(requeue=False) combinada com a DLX garante que mensagens malformadas sejam isoladas imediatamente para análise manual ou reprocessamento offline.

3. Persistência Física (fsync)

Por padrão, o RabbitMQ grava mensagens persistentes no disco, mas a sincronização física (fsync) ocorre em lotes para otimizar a performance. Em cenários de missão crítica onde a perda de mensagens é inaceitável, utilize Quorum Queues em vez de Classic Queues. Elas utilizam o algoritmo de consenso Raft para replicar o estado entre múltiplos nós antes de confirmar a escrita ao publicador.

4. Timeouts de Consumidor

Se um agente demorar mais do que o limite configurado no RabbitMQ (consumer_timeout) para processar uma tarefa, o broker fechará o canal de comunicação. Para tarefas longas de IA ou processamento de dados, delegue a computação pesada para um pool de threads separado e envie batimentos cardíacos (heartbeats) periódicos para manter a conexão AMQP ativa.


Navegação da Trilha

Este artigo faz parte da trilha de desenvolvimento de sistemas autônomos de alta confiabilidade do Zehro Digital.

Rolar para cima