A2A Protocol

This commit is contained in:
2026-10-21 08:23:55 -03:00
parent 1942f78df8
commit e3bd48cf4b
32 changed files with 2141 additions and 12 deletions

248
docs/A2A_ASSINCRONO.md Normal file
View File

@@ -0,0 +1,248 @@
# Agentes do framework com A2A assíncrono
## O que foi implementado
O agente criado a partir de `templates/agent_template_backend` pode expor o
protocolo nativo e A2A no mesmo processo. O Agent Gateway também oferece a
mesma fachada A2A. As duas fachadas usam o SDK oficial e suas operações de
Tasks/SSE, com interoperabilidade A2A 1.0 e 0.3.
```text
Consumidor A2A ── Gateway A2A (Task)
├── agente nativo (/gateway/message)
└── agente framework A2A (Task remota)
Consumidor A2A ── agente framework A2A diretamente
```
O envio imediato devolve uma Task antes de terminar a operação de negócio.
A execução continua após a resposta HTTP e também após a desconexão do SSE.
O consumidor pode consultar o resultado ou se reconectar à tarefa em andamento.
Os endpoints nativos conservam seu comportamento; `/gateway/message` continua
aguardando o resultado, e o SSE nativo permanece separado do SSE A2A.
## Habilitar no agente construído com o framework
Na raiz do framework, instale a dependência opcional:
```sh
python -m pip install -e 'libs/agent_framework[a2a]' -r templates/agent_template_backend/requirements.txt
```
No ambiente do backend:
```dotenv
ENABLE_A2A=true
A2A_TASK_MODE=true
A2A_PUBLIC_URL=http://localhost:8000
A2A_TASK_TIMEOUT_SECONDS=240
```
Inicie o backend com suas configurações normais. Por exemplo, após preparar o
`.env` e os arquivos de configuração do agente:
```sh
cd templates/agent_template_backend
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000
```
O endereço público real deve ser usado em `A2A_PUBLIC_URL`. A configuração de
exemplo está em `.env.a2a.example`. Há também uma imagem opcional, construída
na raiz do framework:
```sh
docker build -f templates/agent_template_backend/Dockerfile.a2a -t framework-agent-a2a .
```
Os perfis de `agents.yaml` são publicados como skills no Agent Card:
`GET /.well-known/agent-card.json`. Use `message.metadata.agent_id` para escolher
um perfil; sem ele, o backend mantém sua seleção padrão.
## Registrar esse agente no Agent Gateway
```yaml
backends:
pedidos_a2a:
protocol: a2a
a2a_version: '1.0'
url: http://backend-pedidos:8000
a2a_rpc_path: /a2a
description: Consulta e acompanhamento de pedidos
keywords: [pedido, entrega, rastreamento]
default_agent_id: pedidos
```
No gateway, habilite `ENABLE_A2A=true`, `A2A_TASK_MODE=true` e configure seu próprio
`A2A_PUBLIC_URL`. Para uma execução mais longa, ajuste os limites, por exemplo:
`A2A_TASK_TIMEOUT_SECONDS=300` e `BACKEND_TIMEOUT_SECONDS=270`, com o timeout do
backend em 240 segundos. A duração efetiva é limitada pelo menor prazo aplicável.
O gateway devolve sua própria Task imediatamente e consulta a Task remota até
conclusão ou solicitação de input/autenticação. Eventos de status remoto são
publicados como `a2a.backend.status`. Os IDs local e remoto podem ser distintos.
O resultado nativo inclui `metadata.a2a_task_id` e `metadata.a2a_context_id` para
correlação; o consumidor externo continua acompanhando o ID emitido pelo gateway.
## Envio assíncrono A2A 1.0
Envie um `POST /a2a` com `Content-Type: application/json` e
`A2A-Version: 1.0`:
```json
{
"jsonrpc": "2.0",
"id": "request-1",
"method": "SendMessage",
"params": {
"configuration": {"returnImmediately": true},
"message": {
"messageId": "message-1",
"contextId": "conversation-123",
"role": "ROLE_USER",
"parts": [{"text": "Consulte meu pedido"}],
"metadata": {"agent_id": "pedidos", "tenant_id": "default"}
}
}
}
```
A resposta contém `result.task.id`, `contextId` e um estado inicial como
`TASK_STATE_SUBMITTED` ou `TASK_STATE_WORKING`. Guarde esse ID e consulte:
```json
{
"jsonrpc": "2.0",
"id": "request-2",
"method": "GetTask",
"params": {"id": "ID_DA_TAREFA"}
}
```
O resultado está em `result`. Quando `status.state` for `TASK_STATE_COMPLETED`,
o conteúdo final estará em `artifacts[].parts`. Há uma parte de texto e uma
parte `data` com a resposta original do framework.
Sem `returnImmediately: true`, o envio aguarda o estado final ou interrompido,
retornando uma Task no mesmo formato. Para manter o comportamento da primeira
entrega A2A, que retornava Message, configure `A2A_TASK_MODE=false`; nesse modo,
streaming fica desabilitado e não há Tasks locais.
## Streaming e reconexão
Para enviar e acompanhar, use `SendStreamingMessage` no lugar de `SendMessage`
com os mesmos parâmetros. A resposta tem `Content-Type: text/event-stream`.
Cada campo `data:` contém uma resposta JSON-RPC. Os eventos contêm:
1. A Task inicial.
2. Atualizações de status: submitted/working e eventos de progresso.
3. O artefato de resposta.
4. O estado completed, failed, canceled, input-required ou auth-required.
O backend do framework publica marcos como `flow.start`, `session.upserted`,
`workflow.started`, `workflow.completed` e `message.responded`, quando o fluxo
passa por essas etapas. Eles aparecem em
`result.statusUpdate.metadata.framework_event`, com os dados correspondentes
em `framework_payload`. Isso não implica transmissão de tokens do LLM.
Para se reconectar a uma Task em andamento:
```json
{
"jsonrpc": "2.0",
"id": "request-3",
"method": "SubscribeToTask",
"params": {"id": "ID_DA_TAREFA"}
}
```
A reconexão entrega o estado atual e os eventos seguintes; não há promessa de
replay de todos os eventos perdidos. Se a tarefa já terminou, consulte `GetTask`.
## Cancelamento, falhas e continuidade
`CancelTask`, com `params.id`, cancela a execução local em andamento. O SDK
interrompe a coroutine e registra `TASK_STATE_CANCELED`. Isso não desfaz efeitos
já confirmados em ferramentas ou sistemas externos. Quando o gateway está
consultando um agente remoto, parar a tarefa local não garante cancelar a
operação que já foi aceita pelo serviço remoto.
Exceções e expiração de `A2A_TASK_TIMEOUT_SECONDS` resultam em
`TASK_STATE_FAILED`, consultável depois, com mensagem genérica. Detalhes internos
permanecem nos logs do servidor.
Para uma interação que exige nova entrada, um handler pode retornar
`metadata.a2a_task_state: "input-required"` ou `"auth-required"`. O consumidor
envia outra mensagem com o mesmo `taskId` e `contextId`. A tarefa é retomada;
uma tarefa concluída não pode ser reexecutada. O adaptador de saída preserva a
continuidade de tarefas remotas nesses estados.
Os workflows nativos que não declaram esse estado concluem uma Task por turno;
a conversa continua com uma nova Task e o mesmo `contextId`, usando as sessões
e memórias já existentes no framework.
## Equivalentes A2A 0.3
Use `A2A-Version: 0.3`, `role: "user"` e os discriminadores `kind` de 0.3.
| A2A 1.0 | A2A 0.3 |
| --- | --- |
| `SendMessage` + `returnImmediately: true` | `message/send` + `blocking: false` |
| `SendStreamingMessage` | `message/stream` |
| `GetTask` | `tasks/get` |
| `SubscribeToTask` | `tasks/resubscribe` |
| `CancelTask` | `tasks/cancel` |
Em 0.3, `result` contém diretamente a Task, sem o envelope `task`. Os estados
usam `submitted`, `working`, `completed`, `failed`, etc.
## Armazenamento e autenticação
O padrão usa o TaskStore em memória do SDK. Tarefas e resultados são perdidos
quando o processo reinicia. Para múltiplos workers/réplicas, use afinidade de
requisições ao mesmo processo durante a execução ou configure armazenamento e
coordenação compartilhados. Um TaskStore persistente, sozinho, não torna a
execução retomável após falha. As funções `install_a2a` e `install_backend_a2a`
aceitam `task_store` para integração com um armazenamento do SDK.
O backend opcional reutiliza `AGENT_AUTH_*`; o gateway usa
`AGENT_GATEWAY_AUTH_*`. A identidade autenticada pelo middleware define o dono
das Tasks, incluindo consultas e cancelamento. Usuários anônimos compartilham
o escopo anônimo. `tenant_id` em metadata continua sendo dado do pedido, não
credencial nem isolamento de autorização. Clientes que compartilham uma mesma
credencial/principal compartilham o escopo de tarefas.
`A2A_SECURITY_SCHEMES` e `A2A_SECURITY` documentam os mecanismos no Agent Card,
como descrito no guia geral. SSE está habilitado no modo de Tasks;
notificações push e gRPC continuam fora desta implementação.
## Validação
A suíte inclui envio imediato enquanto o handler permanece bloqueado, polling,
streaming com progresso e resultado, cancelamento, falhas, timeout, retomada,
separação entre identidades autenticadas e gateway chamando um backend A2A real
em aplicação local. Há também testes HTTP em `127.0.0.1` que verificam a chegada
do primeiro evento antes da conclusão e reconexão após desconexão.
```sh
python -m pip install pytest pytest-asyncio uvicorn
PYTHONPATH=libs/agent_framework/src pytest \
tests/test_a2a_interoperability.py tests/test_a2a_async_tasks.py \
tests/test_a2a_http_streaming.py \
tests/unit/test_sse.py tests/unit/test_sse_replay_dedup.py \
tests/unit/test_authentication.py tests/unit/test_authentication_policies.py -q
```
Os testes não acessam OCI nem serviços de negócio externos. Em ambientes sem
permissão para abrir portas locais, execute o teste HTTP em um ambiente que
permita bind em loopback.
Especificação oficial: https://a2a-protocol.org/latest/specification/
SDK oficial: https://github.com/a2aproject/a2a-python
## Frontend pronto para A2A
A interface `apps/agent_frontend_a2a` consome este protocolo diretamente, com
Tasks, streaming, consulta e cancelamento. Consulte
[Frontend A2A](../apps/agent_frontend_a2a/README.md) para a demonstração sem OCI
e os comandos de execução com o backend ou Gateway. O frontend nativo continua
em `apps/agent_frontend`.

145
docs/A2A_IMPLEMENTACAO.md Normal file
View File

@@ -0,0 +1,145 @@
# Integração A2A no Agent Framework OCI
## Decisão de arquitetura
O Agent Gateway existente passa a ter uma fachada A2A opcional. Não é necessário
manter um segundo roteador, duplicar o supervisor ou migrar os agentes atuais.
```text
Consumidor atual ── /gateway/message ─┐
├─ Agent Gateway / supervisor ─┬─ backend native
Consumidor A2A ──── /a2a ─────────────┘ └─ backend a2a
```
O backend nativo continua recebendo `{channel, payload, agent_id, tenant_id}`.
A ausência de `protocol` no catálogo mantém `native`. A2A fica desativado por
padrão; o SDK só é importado ao habilitar a fachada ou selecionar um backend A2A.
Não foi alterado o runtime dos agentes, os workflows, os canais ou os MCPs.
A entrada usa A2A **1.0 / JSON-RPC**, SDK oficial `a2a-sdk==1.2.2`, com adaptação
0.3 disponibilizada pelo próprio SDK. Agentes externos podem ser registrados
com `a2a_version: '1.0'` ou `'0.3'`; não há detecção heurística de versão.
## Ativação local
Na raiz do framework:
```sh
python -m pip install -e 'libs/agent_framework[a2a]' -r apps/agent_gateway/requirements.txt
export ENABLE_A2A=true
export A2A_PUBLIC_URL=https://meu-gateway.example
export BACKENDS_CONFIG_PATH="$PWD/apps/agent_gateway/config/backends.a2a.example.yaml"
python -m uvicorn app.main:app --app-dir apps/agent_gateway --port 8010
```
Edite primeiro as URLs do catálogo de exemplo. Para agentes externos, informe o
endpoint JSON-RPC anunciado no Agent Card como `url` + `a2a_rpc_path`.
Credenciais ficam em variáveis de ambiente referenciadas por `a2a_headers_env`.
Os valores não são publicados em `/backends`.
A imagem opcional pode ser construída a partir da raiz:
```sh
docker build -f apps/agent_gateway/Dockerfile.a2a -t agent-gateway-a2a .
```
Defina `A2A_PUBLIC_URL` no ambiente do container e monte o catálogo de backends.
A imagem e os comandos anteriores de implantação continuam disponíveis.
## Descoberta e chamada
O Agent Card fica em `GET /.well-known/agent-card.json`; as skills são derivadas
dos backends habilitados. As mensagens chegam em `POST /a2a`.
Exemplo A2A 1.0, com cabeçalho `A2A-Version: 1.0`:
```json
{
"jsonrpc": "2.0",
"id": "request-1",
"method": "SendMessage",
"params": {
"message": {
"messageId": "message-1",
"contextId": "conversation-123",
"role": "ROLE_USER",
"parts": [
{"text": "Quero consultar minha fatura"},
{"data": {"customer_id": "123"}}
],
"metadata": {"tenant_id": "default"}
}
}
}
```
No modo de tarefas (padrão), a resposta fica em `result.task`. O resultado de
negócio está em `artifacts[].parts`, com texto e uma parte `data` contendo a
resposta do framework. Com `A2A_TASK_MODE=false`, permanece em `result.message`. Reutilize `contextId` nos próximos turnos. No modo 0.3,
use o cabeçalho `A2A-Version: 0.3`, método `message/send`, role `user` e os
campos `kind` da especificação 0.3; o resultado é uma Task diretamente
(ou Message com `A2A_TASK_MODE=false`).
Partes `data` devem conter objetos e são incorporadas ao payload de negócio.
A identidade da sessão e o texto do protocolo prevalecem sobre esses dados.
Arquivos recebidos pela fachada são rejeitados explicitamente.
## Conversas, tarefas e limites
- A fachada cria **Tasks A2A** e permite envio imediato, consulta, cancelamento
e streaming SSE. `A2A_TASK_MODE=false` mantém o modo anterior de Messages.
Consulte [A2A assíncrono](A2A_ASSINCRONO.md) para os exemplos completos.
- O adaptador de saída aceita Message ou Task; consulta `GetTask`/`tasks/get`
enquanto o agente está trabalhando, dentro de um prazo total configurável
por `BACKEND_TIMEOUT_SECONDS`. Não repete automaticamente operações de envio.
- Tarefas que pedem input/autenticação preservam estado e identidade para o
próximo turno. Tarefas concluídas não são reutilizadas.
- A continuidade remota é guardada no processo, por tenant/backend/sessão,
com validade de uma hora e limite de 10 mil entradas. Em múltiplas réplicas,
use afinidade de sessão ou implemente armazenamento compartilhado antes de
depender dessa continuidade. Reiniciar o processo limpa esse cache.
- `metadata.a2a_result` preserva o resultado remoto, incluindo dados/artefatos;
`a2a_context_id`, `a2a_task_id` e `a2a_state` permitem inspeção e continuidade
explícita no payload quando necessário.
- Streaming SSE está disponível no modo de tarefas. Push notifications e gRPC
não são oferecidos. O proxy de eventos nativo não é utilizado para agentes
A2A: use `SendStreamingMessage`/`SubscribeToTask` e seus equivalentes 0.3.
- O template `templates/agent_template_backend` também expõe A2A diretamente
quando `ENABLE_A2A=true`, sem passar pelo gateway global.
## Autenticação
As rotas A2A compartilham o middleware de autenticação já instalado no gateway
(`AGENT_GATEWAY_AUTH_*`). Habilitar A2A não habilita autenticação automaticamente.
O tenant em metadata mantém a semântica atual do framework: é dado do pedido,
não uma autorização concedida pelo protocolo. Políticas de acesso a tenants
continuam sendo responsabilidade da configuração de autenticação/autorização.
Quando houver autenticação, declare-a também no Agent Card, por exemplo:
```sh
export A2A_SECURITY_SCHEMES='{"gatewayBearer":{"httpAuthSecurityScheme":{"scheme":"bearer"}}}'
export A2A_SECURITY='[{"gatewayBearer":[]}]'
```
Essas declarações documentam o mecanismo; quem verifica as credenciais é o
middleware. Se desejar descoberta pública, adicione somente o caminho do Agent
Card à configuração de caminhos públicos; `/a2a` permanece protegido.
## Validação
Testes em `tests/test_a2a_interoperability.py` cobrem os dois formatos de entrada,
o contrato nativo, os dois formatos de saída, polling, erros JSON-RPC,
continuidade de tarefas, separação entre tenants e limite de tempo.
```sh
PYTHONPATH=libs/agent_framework/src pytest tests/test_a2a_interoperability.py -q
```
Validação com transportes HTTP simulados e aplicação FastAPI local; não substitui
um teste de integração com os serviços OCI e agentes externos da implantação.
Referências oficiais:
- https://a2a-protocol.org/latest/specification/
- https://a2a-protocol.org/v0.3.0/specification/
- https://github.com/a2aproject/a2a-python