Retrofit, não reconstrução: overlays agênticos para modernizar serviços legados

O problema: sistemas REST em um mundo agêntico

As arquiteturas corporativas foram construídas ao longo de anos sobre APIs REST e microsserviços. Esses sistemas são estáveis, bem testados e estão profundamente enraizados em ambientes produtivos. O problema é que eles não foram projetados para o modelo de comunicação Agent-to-Agent (A2A) — o padrão emergente para agentes autônomos que colaboram, raciocinam e se coordenam por meio de mensagens estruturadas.

Com a consolidação do A2A como padrão, as empresas se veem diante de um desafio duplo: não só precisam integrar serviços REST tradicionais a esse novo modelo, como também precisam trazer agentes que já existem — mas foram construídos como APIs REST — para dentro do ecossistema A2A. Reconstruir tudo do zero não é viável. É aí que entra o conceito de agentic overlay.

REST vs. A2A: paradigmas diferentes

Para entender o problema, vale diferenciar os dois modelos. Uma API REST é projetada para integração determinística cliente-servidor: o cliente chama um endpoint bem definido, passa parâmetros e recebe uma resposta previsível, geralmente em um fluxo stateless baseado em HTTP. Isso torna o REST excelente para expor capacidades de negócio com contratos claros e simplicidade operacional.

Já o A2A foi projetado para interoperabilidade entre agentes autônomos. Os agentes se descobrem por meio de metadados (como um “agent card”), negociam capacidades e trocam mensagens estruturadas — frequentemente via JSON-RPC (Notação de Objeto JavaScript para Chamada de Procedimento Remoto) — para coordenar tarefas em múltiplos passos. Enquanto o REST otimiza para interfaces de serviço estáveis e execução direta, o A2A otimiza para coordenação orientada a raciocínio, mensagens orientadas a tarefas e colaboração entre agentes.

A solução: agentic overlays

Um agentic overlay é uma camada fina de encapsulamento que permite que serviços baseados em REST participem de comunicações A2A. Essa camada realiza duas funções principais:

O ponto central é que o A2A não é uma nova API — é uma nova interface para uma API que já existe. O serviço REST subjacente permanece intocado.

Abordagens alternativas (e por que são problemáticas)

Antes de detalhar o overlay, a AWS compara essa abordagem com outras alternativas comuns:

Manter stacks REST e A2A separados

Essa abordagem significa manter dois conjuntos de endpoints (/api/v2/... e /a2a/...), duas implementações de autenticação e validação, dois pipelines de deploy e duplo trabalho de observabilidade. O risco de inconsistência entre os dois caminhos é alto, e o custo operacional cresce com o tempo.

Stacks separados com lógica de negócio compartilhada

Aqui, a ideia é refatorar os endpoints existentes para que a lógica de negócio possa ser reutilizada por uma nova interface A2A. Embora pareça mais limpa, essa abordagem pode introduzir regressões, desvios de comportamento e uma carga de testes considerável — mesmo que os caminhos REST externos permaneçam os mesmos.

Implementando um overlay dentro da aplicação

Nessa abordagem, a aplicação passa a ter dois conjuntos de endpoints (/api/v2/... e /a2a/...), mas mantém um único pipeline de build, teste e deploy. Endpoints REST tradicionais são transformados em endpoints agênticos sem reescrever a lógica central. Novas rotas são adicionadas no mesmo host e porta. As próprias skills do agente podem ser usadas para roteamento interno, sem necessidade de um servidor MCP separado.

A AWS apresenta um exemplo de prova de conceito usando um serviço de calculadora legado em Flask, convertido para o padrão A2A com um overlay. O fluxo de tradução de mensagens A2A funciona assim:

  • Recebe requisições JSON-RPC 2.0
  • Mapeia tarefas A2A para endpoints REST
  • Repassa cabeçalhos de autenticação
  • Chama endpoints REST internamente
  • Traduz respostas REST para o formato JSON-RPC

Comparação de formatos: REST vs. A2A

Para ilustrar a diferença, veja como a mesma operação de soma é representada nos dois protocolos.

Requisição de entrada — REST vs. A2A:

REST:
{ "operation": "add", "operands": [5, 3] }

A2A:
{
  "jsonrpc": "2.0",
  "method": "SendMessage",
  "params": {
    "message": {
      "role": "user",
      "parts": [
        {
          "kind": "data",
          "data": { "operation": "add", "operands": [5, 3] }
        }
      ]
    }
  },
  "id": 1
}

Resposta de saída — REST vs. A2A:

REST:
{"result": 8}

A2A:
{
  "jsonrpc": "2.0",
  "result": {
    "messageId": "uuid",
    "contextId": "uuid",
    "role": "agent",
    "parts": [{"kind": "data", "data": {"result": 8}}],
    "kind": "message",
    "metadata": {}
  },
  "id": 1
}

Estrutura do overlay em Flask

O overlay é composto por componentes bem definidos. O agent card é construído dinamicamente e expõe as capacidades e skills do agente:

A2A_API_URL = "http://localhost:5000/a2a"
EXECUTE_TIMEOUT_SECONDS = 30

_SKILLS_FILE = Path(__file__).parent / "skills.json"
_SKILLS_CACHE: Optional[List[Dict[str, Any]]] = None

def _load_skills() -> List[Dict[str, Any]]:
    global _SKILLS_CACHE
    if _SKILLS_CACHE is not None:
        return _SKILLS_CACHE
    try:
        with open(_SKILLS_FILE) as f:
            _SKILLS_CACHE = json.load(f)
            return _SKILLS_CACHE
    except FileNotFoundError:
        logger.error(f"Skills file not found: {_SKILLS_FILE}")
        return []
    except json.JSONDecodeError as e:
        logger.error(f"Invalid JSON in skills file: {e}")
        return []

def build_agent_card(api_url: Optional[str] = None) -> Dict[str, Any]:
    if api_url is None:
        api_url = A2A_API_URL
    return {
        "name": "Calculator Agent",
        "description": "Simple calculator supporting basic arithmetic operations",
        "supportedInterfaces": [
            {"url": api_url, "protocolBinding": "JSONRPC", "protocolVersion": "0.3"},
        ],
        "provider": {"organization": "Example Organization", "url": ""},
        "version": "1.0.0",
        "capabilities": {
            "streaming": False,
            "pushNotifications": False,
            "extendedAgentCard": False,
        },
        "defaultInputModes": ["text/plain", "application/json"],
        "defaultOutputModes": ["text/plain", "application/json"],
        "skills": _load_skills(),
    }

AGENT_CARD = build_agent_card()

O chamador REST interno repassa autenticação e trata erros de rede:

def invoke_rest_endpoint(
    endpoint: str,
    json_data: Optional[Dict] = None,
    http_method: str = "POST"
) -> Tuple[Optional[Dict], int]:
    try:
        base_url = request.host_url.rstrip("/")
        url = f"{base_url}{endpoint}"
        headers = {"Content-Type": "application/json"}
        auth_header = request.headers.get("Authorization")
        if auth_header:
            headers["Authorization"] = auth_header
        logger.info(f"Adapter: Delegating to REST {http_method} {url}")
        if http_method.upper() == "POST":
            response = http_requests.post(url, json=json_data, headers=headers, timeout=EXECUTE_TIMEOUT_SECONDS)
        elif http_method.upper() == "GET":
            response = http_requests.get(url, headers=headers, timeout=EXECUTE_TIMEOUT_SECONDS)
        else:
            response = http_requests.request(http_method, url, json=json_data, headers=headers, timeout=EXECUTE_TIMEOUT_SECONDS)
        logger.info(f"Adapter: REST returned {response.status_code}")
        return response.json(), response.status_code
    except http_requests.RequestException as e:
        logger.error(f"Adapter: Error calling REST endpoint: {e}", exc_info=True)
        return {"error": "Internal server error"}, 500

A extração do payload da mensagem A2A e a construção da resposta seguem o formato da Spec 0.3:

def extract_message_payload(message: Dict) -> Optional[Dict]:
    try:
        parts = message.get("parts", [])
        for part in parts:
            if isinstance(part, dict) and part.get("kind") == "data":
                return part.get("data")
        return None
    except Exception as e:
        logger.error(f"Error extracting message payload: {e}")
        return None

def build_a2a_message(message_id: str, context_id: str, content: Any) -> Dict:
    if isinstance(content, dict):
        parts = [{"kind": "data", "data": content}]
    else:
        parts = [{"kind": "text", "text": str(content)}]
    return {
        "messageId": message_id,
        "contextId": context_id,
        "role": "agent",
        "parts": parts,
        "kind": "message",
        "metadata": {}
    }

Os construtores de resposta JSON-RPC seguem a especificação 2.0:

class JsonRpcError:
    PARSE_ERROR = -32700
    INVALID_REQUEST = -32600
    METHOD_NOT_FOUND = -32601
    INVALID_PARAMS = -32602
    INTERNAL_ERROR = -32603

def jsonrpc_error(code: int, message: str, data: Any = None, request_id: Any = None) -> Dict:
    response = {
        "jsonrpc": "2.0",
        "error": {"code": code, "message": message},
        "id": request_id
    }
    if data is not None:
        response["error"]["data"] = data
    return response

def jsonrpc_success(result: Any, request_id: Any = None) -> Dict:
    return {
        "jsonrpc": "2.0",
        "result": result,
        "id": request_id
    }

O handler do método SendMessage delega diretamente para o endpoint REST:

def handle_send_message(data: Dict) -> Tuple[Any, int]:
    request_id = data.get("id")
    params = data.get("params", {})
    message = params.get("message", {})
    context_id = message.get("contextId") or generate_id()
    message_id = generate_id()
    payload = extract_message_payload(message)
    if not payload:
        return jsonify(jsonrpc_error(
            JsonRpcError.INVALID_PARAMS,
            "Invalid params: No data found in message.parts.",
            request_id=request_id
        )), 400
    rest_response, status = invoke_rest_endpoint(
        endpoint="/api/v1/calculate",
        json_data=payload,
        http_method="POST"
    )
    if 200 <= status < 300:
        a2a_message = build_a2a_message(message_id, context_id, rest_response)
        return jsonify(jsonrpc_success(a2a_message, request_id)), 200
    else:
        error_message = "Operation failed"
        if isinstance(rest_response, dict):
            error_message = (rest_response.get("error") or rest_response.get("details") or "Operation failed")
        error_code = (JsonRpcError.INVALID_PARAMS if 400 <= status < 500 else JsonRpcError.INTERNAL_ERROR)
        return jsonify(jsonrpc_error(
            error_code,
            error_message,
            data=rest_response,
            request_id=request_id
        )), status

def generate_id() -> str:
    return str(uuid.uuid4())

As rotas A2A são registradas na aplicação Flask:

def setup_a2a_routes(app: Flask) -> None:
    app.add_url_rule("/.well-known/agent-card.json", "get_agent_card", get_agent_card, methods=["GET"])
    app.add_url_rule("/a2a/capabilities", "get_capabilities", get_capabilities, methods=["GET"])
    app.add_url_rule("/a2a/health", "a2a_health", a2a_health, methods=["GET"])
    app.add_url_rule("/a2a", "a2a_jsonrpc", _handle_jsonrpc, methods=["POST"])
    logger.info("A2A Protocol v0.3 routes registered")

E a inicialização da aplicação une as duas partes:

# app/main.py
from flask import Flask
from app.rest_api import rest_api
from app.a2a_adapter import setup_a2a_routes

def create_app():
    app = Flask(__name__)
    app.register_blueprint(rest_api)
    setup_a2a_routes(app)
    app.logger.info("A2A Protocol enabled via Request Translator Pattern")
    return app

Para rodar a aplicação:

python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
pip install -r requirements.txt
python -m app.main

Escala corporativa com Amazon Bedrock AgentCore Gateway

Para cenários de maior escala, a AWS apresenta o Amazon Bedrock AgentCore como alternativa para desacoplar o overlay da aplicação. O AgentCore Runtime atua como ponto de acesso único para endpoints e serviços, permitindo que um único overlay agêntico sirva múltiplos serviços — não apenas um.

O AgentCore Gateway suporta até 10 targets por gateway, com integração nativa a serviços AWS e suporte a endpoints OpenAPI. Isso é especialmente útil em aplicações corporativas que orquestram múltiplos serviços para tarefas complexas: em vez de adicionar um overlay a cada serviço individualmente, é possível unir vários serviços em um único overlay que orquestra as chamadas conforme necessário.

Além do Gateway, o ecossistema AgentCore oferece:

  • AgentCore Identity: gerencia autenticação para componentes de agente e gateway, com suporte a provedores OAuth 2.0 e integrações gerenciadas para Okta, GitHub e Slack.
  • AgentCore Observability: monitora a performance dos agentes por meio de métricas, logs e visualizações de span. Permite visualizar dados de alto nível como chamadas de ferramentas e latência, ou inspecionar caminhos de execução granulares entre componentes, com integração nativa ao Amazon CloudWatch.
  • AgentCore Runtime: realiza o deploy de modelos via imagem de container — sejam open-source, customizados ou Modelos de Linguagem de Grande Escala (LLMs) do Amazon Bedrock como Nova e Anthropic Claude — sem exigir que o time gerencie a infraestrutura de LLM.

Próximos passos recomendados

A AWS sugere três ações práticas para quem quer explorar esse padrão:

  • Avalie sua arquitetura: faça um levantamento dos serviços REST candidatos à habilitação A2A. Overlays dentro da aplicação são indicados para agentes de serviço único; o AgentCore Gateway é mais adequado para fluxos multi-serviço.
  • Revise a implementação de referência: o exemplo da calculadora em Flask demonstra o padrão de tradução com configuração do agent card, extração de mensagens, invocação REST e construção de respostas.
  • Explore o Amazon Bedrock AgentCore: Gateway, Identity e Observability oferecem infraestrutura para overlays agênticos em produção. A especificação e a documentação do SDK do protocolo A2A estão disponíveis em a2a-protocol.org, com bibliotecas para Flask, FastAPI e Starlette.

Conclusão

O padrão de agentic overlay oferece às empresas um caminho pragmático para adotar comunicação Agent-to-Agent sem abandonar os investimentos em APIs REST. A ideia central é simples e poderosa: o A2A não é uma nova API — é uma nova interface para a API que já existe. Essa perspectiva muda o desafio de reconstruir tudo para uma modernização incremental, permitindo que as organizações capturem valor de IA mais rapidamente enquanto gerenciam riscos de forma eficaz.

Fonte

Retrofit, don’t rebuild: Agentic overlays for transforming legacy enterprise services (https://aws.amazon.com/blogs/machine-learning/retrofit-dont-rebuild-agentic-overlays-for-transforming-legacy-enterprise-services/)

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *