mirror of
https://github.com/mcloh/Enterprise-MCP-Service-Bus.git
synced 2026-11-05 22:14:11 +00:00
revisão e complementação da documentação do projeto
This commit is contained in:
125
RI/docs/interfaces/agent-runtime-dispatch.md
Normal file
125
RI/docs/interfaces/agent-runtime-dispatch.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# 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`](../../src/emcp_bus/agent_runtime/dispatcher.py),
|
||||
[`agent_runtime/global_supervisor.py`](../../src/emcp_bus/agent_runtime/global_supervisor.py),
|
||||
[`agent_runtime/handoff_graph.py`](../../src/emcp_bus/agent_runtime/handoff_graph.py),
|
||||
[`agent_runtime/identity_resolver.py`](../../src/emcp_bus/agent_runtime/identity_resolver.py),
|
||||
[`agents/example_agent/mode_b_orchestrator_driven/orchestrator.py`](../../agents/example_agent/mode_b_orchestrator_driven/orchestrator.py).
|
||||
Ver [`AS-BUILT.md`](../AS-BUILT.md) (seções Agent Runtime, Modo A, Modo B).
|
||||
|
||||
## `AgentDispatcher` — o primitivo comum aos dois modos
|
||||
|
||||
```python
|
||||
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).
|
||||
|
||||
```python
|
||||
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.
|
||||
|
||||
```python
|
||||
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`](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.
|
||||
|
||||
```python
|
||||
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 `NBADecision`s
|
||||
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
|
||||
|
||||
```python
|
||||
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` |
|
||||
Reference in New Issue
Block a user