Files
Enterprise-MCP-Service-Bus/RI/docs/interfaces/agent-runtime-dispatch.md
2026-09-14 14:01:27 -03:00

7.0 KiB

Interface: Agent Runtime dispatch (AgentDispatcher, GlobalSupervisor, ModeBOrchestrator)

Contratos de despacho do Agent Runtime, para quem vai construir um novo backend de agente ou uma nova integração multiagente seguindo o docs/adr/ADR-024-posicionamento-multiagente.md. Implementação: src/emcp_bus/agent_runtime/dispatcher.py, agent_runtime/global_supervisor.py, agent_runtime/handoff_graph.py, agent_runtime/identity_resolver.py, agents/example_agent/mode_b_orchestrator_driven/orchestrator.py. Ver AS-BUILT.md (seções Agent Runtime, Modo A, Modo B).

AgentDispatcher — o primitivo comum aos dois modos

result = await dispatcher.dispatch(nba: NBADecision, arguments: dict[str, Any]) -> DispatchResult

nba (ver orchestrator/nba_model.py) carrega agent — o nome do agente cuja credencial deve ser usada. AgentDispatcher nunca gera ou guarda credencial própria: obtém o token via AgentTokenProvider.token_for(nba.agent), tipicamente um client_credentials real contra o mesmo IdP de qualquer outro MCP Client (OIDCAgentTokenProvider), e chama nba.action através do Gateway real — a mesma reautorização de qualquer outro tools/call (EP-05/EP-03).

DispatchResult(decision_id="...", action="finance.payment.execute", status="allowed" | "denied",
                structured_content={...} | None, error_text: str | None)

Uma negação do Gateway/PDP vira status="denied" com error_text — dispatch() nunca reinterpreta ou reexecuta em torno de uma negação; recálculo de NBA após DENY é responsabilidade de uma camada acima (orchestrator/fallback.handle_denial).

Modo A — GlobalSupervisor (governança-somente, ADR-024)

Para quando uma plataforma de orquestração externa decide qual agente atende a conversa; o Service Orchestrator (EP-11) não participa.

supervisor = GlobalSupervisor(backends={"sales_agent": ..., "finance_agent": ...})
result = await supervisor.route_and_run(
    message, current_backend="sales_agent", router=keyword_router
)
# RoutingResult(backend_name, handed_off: bool, turn_result)
  • router: RouterFn — Callable[[str], str | None], escolhe um nome de backend a partir do conteúdo da mensagem, ou None para permanecer no backend atual. keyword_router é a implementação de referência (palavra-chave, deliberadamente simples) — não é a fronteira de segurança; um roteamento errado, ou uma tentativa de manipular o roteamento via prompt, é sempre recapturado pelo Gateway/PDP real no backend que efetivamente responde.
  • BackendAgent — o único contrato que um backend precisa satisfazer: async def run_turn(message: str) -> object. Cada BackendAgent resolve seu próprio MCP Client/entitlement de forma completamente independente (token, tools/list, reautorização) — GlobalSupervisor nunca lê, guarda ou repassa a credencial de um backend para outro.
  • GlobalSupervisor nunca importa emcp_bus.orchestrator (verificado via análise de AST no teste unitário correspondente) — prova mecânica de que o Modo A não usa NBA própria.

Para integrar um novo backend de agente no Modo A: implementar BackendAgent.run_turn, registrar seu próprio ClientRegistration/ClientProfile (ver identity-oidc.md) como qualquer outro MCP Client, e adicioná-lo ao dict backends do GlobalSupervisor.

Modo B — ModeBOrchestrator (substituição completa, ADR-024)

Para quando o nosso próprio Service Orchestrator é o único cérebro de NBA/NBO — nenhum Global Supervisor externo é consultado.

orchestrator = ModeBOrchestrator(
    graph=journey_graph,
    dispatcher=dispatcher,
    client_ids=["sales-read-agent", "finance-payments-agent"],
)
decision = orchestrator.decide(subject_id="subject:7f31c2", channel="chat")
# ModeBDecision(chosen: NBADecision, considered: list[NBADecision]) | None
result = await orchestrator.decide_and_dispatch(subject_id=..., channel=..., arguments={...})

graph é o journey StateGraph real do EP-11-T02 (orchestrator/graph.py, build_journey_graph), invocado sem modificação, uma vez por client_id em client_ids, para o mesmo subject_id — cada invocação resolve profile/entitlement/offerings daquele client de forma independente. _combine é a regra de referência que decide entre os NBADecisions resultantes (pendência de Finance sempre vence oferta de Sales) — não é uma reivindicação de otimalidade, apenas a mesma postura de RuleBasedNBADecisionModel (EP-11-T03). Um client_id cuja jornada termina sem oferta sobrevivente (nba is None) simplesmente não contribui a considered — nunca um erro.

Para estender a regra de combinação: substituir ModeBOrchestrator._combine por um modelo de ranking cross-domain real — o contrato de entrada/saída (list[NBADecision] → NBADecision | None) não muda.

execute_handoff/build_handoff_payload (EP-12-T03) — implementado, não usado pelos modos A/B

Primitivo de handoff cross-process com dois cuidados estruturalmente separados: minimização de contexto (build_handoff_payload, só carry_keys sobrevive) e resolução de entitlement sempre independente (execute_handoff despacha via o mesmo AgentDispatcher, que resolve a credencial do agente destino do zero). Achado real: nem o Modo A (roteamento conversacional puro, sem uma NBADecision de origem) nem o Modo B (já combina decisões e despacha o vencedor direto) usam este primitivo — ver AS-BUILT.md para o detalhe. Continua correto e reutilizável para um handoff dentro de uma jornada Orchestrator-driven que precise ceder controle a outro agente no meio da execução.

IdentityResolver (EP-12-T04) — nunca uma entrada de autorização

resolver = IdentityResolver(IdentityResolverConfig(aliases={"msisdn": "customer_key"}))
resolver.resolve(
    {"msisdn": "5511...", "amount": 100}
)  # -> {"customer_key": "5511...", "amount": 100}

Normaliza nomes de campo entre canais para o mapeamento de parâmetros do Fabric/MCP — nunca consultado pelo PDP (verificado via AST no teste unitário correspondente, mesma disciplina do GlobalSupervisor). Um campo resolvido a partir de dado fornecido pelo caller nunca deve influenciar uma decisão de autorização (a lição do docs/adr/ADR-023-autorizacao-nunca-vem-do-payload.md).

Erros

Situação Exceção
nba.agent sem AgentTokenProvider configurado UnknownAgentError
IdP rejeita a obtenção do token do agente AgentCredentialError
current_backend não registrado no GlobalSupervisor UnknownBackendError
nba.agent do handoff diverge de payload.target_agent ValueError (execute_handoff)
Canal sem ChannelAdapter registrado UnsupportedChannelError