# Multi-Agent Patterns — Composing agents

> **Scope:** stacks=["dotnet"]
> **Layer:** 1 (on-keyword)
> **Keywords:** multi-agent, agent as tool, agent composition, sequential agents, handoff, sub-agent, agent orchestration, agente auxiliar, nesting
> **Read by Claude in:** plan (quando ai-agents-sweet-spot indica multi-agent) + implement

**Verified against:** Microsoft.Agents.AI 1.20.0 (ai-pin 2026-09-08) + Microsoft.Agents.AI.Workflows 1.20.0 (**fora do pin**; versão apurada no nuget.org em 2026-09-08). **Verificação documental**, sem cláusula `provado por` — o ai-kit não compõe agentes. As assinaturas de `AgentWorkflowBuilder`/`HandoffWorkflowBuilder` e a **ausência** do atributo `[Experimental]` no Handoff foram **medidas por reflexão sobre a assembly 1.20.0** em 2026-09-08. Last-verified: 2026-09-08.

---

## Quando multi-agent (e quando não)

Leia `ai-agents-sweet-spot` antes de continuar. O default é **single agent**.

Multi-agent só se justifica quando:

1. Existem **tópicos genuinamente distintos** que um único prompt não gerencia bem (ex: classificar *e* redigir com especialização independente).
2. Há um **handoff claro** entre responsabilidades — não apenas "dividir o código em dois agents".
3. Um Agent-as-Tool não resolve (o sub-agent precisa ser invocado *várias vezes* ou em paralelo).

> "Em dúvida → single agent. Escalate quando dói, não antes."

---

## Agent-as-Tool (padrão recomendado)

**O padrão mais simples de composição.** O agent pai decide *quando* chamar o agent filho — exatamente como chama qualquer outra tool. Não é um Workflow; sem edges, sem builders.

### Caso canônico: classifier + drafter (suporte)

Um `ClassifierAgent` categoriza o ticket. Um `DrafterAgent` recebe a categoria e escreve a resposta. O Drafter não precisa saber de classificação — só recebe a categoria como contexto.

```csharp
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

// --- 1. Agents via Model Registry alias (nunca hardcode modelo ou provider) ---
IChatClient chatClient = _modelRegistry.GetChatClient("text-default");

AIAgent classifierAgent = chatClient.AsAIAgent(
    instructions: "Classify the support ticket into one of: billing, technical, general. Reply with only the category.",
    name: "ClassifierAgent");

// --- 2. Expõe o classifier como tool usando .AsAIFunction() ---
AIFunction classifierTool = classifierAgent.AsAIFunction();

// --- 3. Drafter recebe o classifier como tool ---
AIAgent drafterAgent = chatClient.AsAIAgent(
    instructions: """
        You are a support reply writer. When you receive a ticket:
        1. Call ClassifierAgent to get the category.
        2. Write a concise, professional reply appropriate for that category.
        """,
    name: "DrafterAgent",
    tools: [classifierTool]);

// --- 4. Executa --- o Drafter chama o Classifier automaticamente quando necessário ---
AgentResponse response = await drafterAgent.RunAsync(ticketText);
string reply = response.Text;
```

**Key points:**
- `agent.AsAIFunction()` — método direto para expor um `AIAgent` local como tool.
- Use `AIFunctionFactory.Create(...)` quando precisar sobrescrever o nome/descrição da tool ou encapsular lógica adicional ao chamar o agent.
- O agent pai decide *se* e *quando* invocar o filho — o LLM controla o fluxo.
- Ambos os agents usam o mesmo alias do Model Registry; podem usar aliases distintos se o caso de uso justificar.

---

## Sequential (cadeia fixa)

Quando a ordem é fixa A→B→C e cada etapa depende da anterior.

**Na maioria dos casos, C# imperativo é mais simples que um Workflow:**

```csharp
// classifierAgent, drafterAgent, reviewerAgent — each built from _modelRegistry.GetChatClient(...) as shown above
// Sem Workflow, sem builders — só chamadas encadeadas
AgentResponse classified = await classifierAgent.RunAsync(ticketText);
AgentResponse drafted    = await drafterAgent.RunAsync(classified.Text);
AgentResponse reviewed   = await reviewerAgent.RunAsync(drafted.Text);
string finalReply = reviewed.Text;
```

Use `AgentWorkflowBuilder.BuildSequential(agent1, agent2, ...)` quando precisar de **streaming de eventos** entre as etapas da cadeia (ex: exibir progresso passo a passo no front-end). Ver `ai-agents-workflows`.

---

## Handoff (roteamento dinâmico)

Um agent de triagem decide qual especialista assume o controle com base no contexto. Útil quando há N especialistas e a decisão de roteamento é complexa demais para uma tool simples.

**Status: graduou.** O Handoff era marcado experimental no 1.0 e foi anunciado como *"graduating to release"* no BUILD 2026 (2026-06-03). **Medido em 2026-09-08:** `Microsoft.Agents.AI.Workflows.HandoffWorkflowBuilder` e `AgentWorkflowBuilder` **não carregam** `[Experimental]` na assembly 1.20.0 — a graduação está no binário, não só no anúncio.

Para handoff baseado em Workflow: `AgentWorkflowBuilder.CreateHandoffBuilderWith(triageAgent)`. Ver `ai-agents-workflows` para o padrão completo.

**Limites documentados, e o que cada um evita** (assinaturas medidas em `HandoffWorkflowBuilderCore<TBuilder>`):

| Limite | Como | O que evita |
|---|---|---|
| Teto de turnos | `.WithAutonomousMode(turnLimit: 8)` — aceita também `agentTurnLimits` por agente | Loop infinito de dois agentes se devolvendo a conversa |
| Condição de término | `.WithTerminationCondition(...)` | O mesmo, por critério de conteúdo em vez de contagem |
| `Id` estável por agente | Cada `AIAgent` com `Id` fixo (não regenerado a cada boot) | Handoff que aponta para um agente que "sumiu" entre reinícios |
| Group chat pequeno | ≤ **3** agentes por group chat | Acima disso o gerente gasta mais turnos escolhendo quem fala do que resolvendo |

> Para roteamento simples (2-3 caminhos), considere Agent-as-Tool com um agent por caminho antes de ir para Handoff Workflow.

---

## Agente auxiliar: em SEQUÊNCIA, nunca dentro de uma tool

**Anti-padrão medido em campo.** É tentador embrulhar um agente auxiliar (resumidor, classificador, extrator) numa tool do agente principal: parece composição barata. Não é.

```csharp
// ERRADO — o auxiliar roda DENTRO da tool do principal.
AIFunction summarize = AIFunctionFactory.Create(async (string text) =>
{
    var r = await _summarizerAgent.RunAsync(text);   // agente aninhado na tool
    return r.Text;
});
```

**Dois problemas concretos:**

1. **`CallId` com tools encadeadas.** O agente auxiliar tem o próprio ciclo de tool calling. Rodando dentro da tool do principal, dois laços de function-calling se sobrepõem no mesmo turno, e a reconciliação de `CallId` entre pedido e resultado passa a depender da ordem em que os dois laços terminam.
2. **O loop do principal fica aberto enquanto o auxiliar roda.** O principal está no meio de um turno, com uma chamada pendente, esperando algo que pode levar várias voltas de LLM. Timeout, retry e telemetria do turno principal passam a medir a soma dos dois.

**O contrato correto são DUAS chamadas visíveis, em sequência:**

```csharp
// CERTO — o auxiliar roda FORA, e o resultado entra como dado no turno seguinte.
AgentResponse summary = await _summarizerAgent.RunAsync(text);
AgentResponse answer  = await _mainAgent.RunAsync(
    $"Resumo do documento:
{summary.Text}

Pergunta: {question}", session);
```

Cada chamada tem o próprio span, o próprio custo e o próprio timeout — e o orquestrador é o **código**, não o modelo. É o mesmo princípio do compositor determinístico em `ai-agents-context-providers`.

**A exceção, com regra:** `agent.AsAIFunction()` é a composição barata **antes** de qualquer orquestração — e serve exatamente quando o LLM principal precisa **decidir** se chama o auxiliar. Nesse caso vale o teto: **aninhamento ≤ 2 níveis** (pai → filho). Um terceiro nível é sinal de que a composição devia ser sequência no código.

---

## Decision tree

```
Tenho múltiplos agents — qual padrão usar?

├─ Fan-in/out genuíno sobre N inputs (paralelo)?
│   └─ Workflow (ai-agents-workflows)
│
├─ Ordem fixa A→B→C, cada etapa depende da anterior?
│   ├─ Preciso de streaming de eventos entre etapas?
│   │   └─ SIM → AgentWorkflowBuilder.BuildSequential (ai-agents-workflows)
│   └─ NÃO → chamadas encadeadas imperativas em C#
│
├─ Sub-task consultada sob demanda pelo agent pai?
│   └─ Agent-as-Tool (agent.AsAIFunction())  ← padrão mais comum
│
└─ Roteamento por intenção, especialistas distintos?
    └─ Handoff (AgentWorkflowBuilder.CreateHandoffBuilderWith)
```

---

## Anti-patterns

| Anti-pattern | Por que é errado | O que fazer |
|---|---|---|
| Usar multi-agent quando um único agent resolve | Adiciona latência, custo e superfície de erro sem ganho | Single agent com tools. Ver `ai-agents-sweet-spot`. |
| Usar Workflow quando Agent-as-Tool basta | Workflow traz edges, builders, streaming — complexidade desnecessária | `agent.AsAIFunction()` primeiro |
| Passar histórico de conversa bruto entre agents | Output do LLM é não-determinístico; o outro agent não sabe o que fazer com histórico livre | Structured Output entre agents. Ver `ai-agents-structured-output`. |
| Nesting profundo (agent A chama B chama C chama D) | Dificulta debug, amplifica latência, falhas em cascata | Achate a composição: Agent-as-Tool com o agent folha direto no pai; teto de **2** níveis. |
| **Agente auxiliar aninhado dentro de uma tool** | Dois laços de function-calling no mesmo turno (reconciliação de `CallId`) e o turno do principal aberto enquanto o auxiliar roda | Duas chamadas visíveis, auxiliar **em sequência**, fora da tool |
| Handoff sem `turnLimit` nem condição de término | Dois agentes devolvem a conversa um ao outro indefinidamente | `.WithAutonomousMode(turnLimit:)` ou `.WithTerminationCondition(...)` |
| `Id` de agente gerado a cada boot num workflow de handoff | O destino do handoff deixa de existir entre reinícios | `Id` estável, vindo de configuração |
| Group chat com 5+ agentes | O gerente gasta os turnos escolhendo quem fala | ≤ 3 agentes; acima disso, repense a decomposição |
| Agents com responsabilidades sobrepostas | Nenhum dos dois sabe quando agir; o LLM fica confuso | Fronteiras claras de responsabilidade; documente em `decisions.md`. |

---

## Checklist (verifiable by morph-eval)

- [ ] Foi verificado em `decisions.md` que single agent não resolve?
- [ ] O padrão escolhido é o mais simples que atende ao requisito? (Tool < Sequential imperativo < Workflow)
- [ ] `agent.AsAIFunction()` usado para composição local (não `AIFunctionFactory.Create` desnecessariamente)?
- [ ] Agents usando alias do Model Registry — nenhum model string hardcoded?
- [ ] Structured Output entre agents onde o output de um é input do outro?
- [ ] Nesting máximo de 2 níveis (pai → filho) sem cascata profunda?
- [ ] Nenhum agente auxiliar roda dentro de uma tool de outro agente — auxiliares são chamadas em sequência, no código?
- [ ] Todo workflow de handoff tem `turnLimit` **ou** condição de término, e os agentes têm `Id` estável?
- [ ] Group chat (se houver) tem no máximo 3 agentes?

---

## References

- `ai-agents-sweet-spot` — single agent é o default; leia antes deste documento
- `ai-agents-workflows` — Workflow (fan-in/out, Sequential com streaming, Handoff completo)
- `ai-agents-setup` — pacotes, DI, `AsAIAgent`
- `ai-agents-structured-output` — output tipado entre agents
- `ai-agents-providers-model-registry` — Model Registry alias pattern
- `ai-agents-context-providers` — o compositor determinístico: o código decide o modo, o LLM classifica
- `ai-agents-observability-patterns` — por que "uma chamada, um span" importa para medir latência

---

*MORPH-SPEC by Polymorphism Tech — ai-agents/multi-agent-patterns.md v2.0 (2026-09-08)*
