# Conversational Agent with Phases — um agente, fase como dado do turno

> **Scope:** stacks=["dotnet"]
> **Layer:** 2 (sob demanda)
> **Keywords:** agente conversacional, fases do agente, allow-list de tools, RequireAny, ChatToolMode, compositor de turno, teto de tool calls, turno de conversa
> **Read by Claude in:** implement (ao construir ou alterar um agente que atende uma conversa ao longo do tempo)

**Verified against:** Microsoft.Extensions.AI 10.9.0 + Microsoft.Agents.AI 1.20.0 (ai-pin 2026-09-08). `ChatToolMode.RequireAny` foi **medido por reflexão sobre `Microsoft.Extensions.AI.Abstractions` 10.9.0** em 2026-09-08. O restante é **verificação documental**, sem cláusula `provado por` — o ai-kit ainda não traz um compositor de turno, e o padrão é destilado de um único repositório de campo (ver a nota de fonte única abaixo). Last-verified: 2026-09-08.

---

> **Nota de fonte única, declarada no corpo de propósito.** Todo o lastro de campo deste standard vem
> de **um** repositório em produção. Não há segundo exemplar no acervo para confirmar nem para
> contradizer. Isso não invalida o padrão — ele roda, e roda há tempo suficiente para ter dor
> registrada —, mas muda como lê-lo: as **formas** aqui são fortes; os **números** (um teto de 8,
> dois estágios de poda) são o que aquele produto precisou, não um universal. Onde este arquivo diz
> "medido", leia "medido ali".

---

## Quando este arquétipo

`ai-agents-agent-archetypes` decide a forma; este arquivo é o aprofundamento da primeira. Ele vale
quando as quatro condições valem juntas:

1. Há **um interlocutor** e uma conversa que dura mais de um turno.
2. O que o agente **pode fazer** muda conforme o ponto da conversa.
3. Existe estado observável de onde derivar esse ponto — não a memória do modelo.
4. Toda ação com efeito no mundo é uma **tool**, não uma frase.

Falhando (2), você tem um agente simples com histórico e não precisa de nada aqui. Falhando (4), o
teto de tool calls e a allow-list perdem o sentido, porque a ação escapa pelo texto.

---

## Compositor de turno determinístico

**A peça central, e a mais reaproveitável.** Um `static class` puro — sem I/O, sem relógio, sem
banco — que recebe o estado do turno e devolve **um plano de turno**: as instruções, a lista de
tools permitidas, e os limites daquele turno.

Três propriedades definem o padrão:

**(1) Prompt decomposto em blocos nomeados.** O prompt não nasce como uma string; nasce como uma
lista de blocos com nome (`persona`, `contexto do lead`, `regras da fase`, `histórico`, …). O nome
serve para medir: dá para dizer que bloco cresceu, que bloco entrou, que bloco não deveria estar ali.

**(2) O texto enviado É a concatenação dos blocos, derivada sem cache.** Esta é a invariante, e ela
é deliberadamente cara: nada de guardar a string montada ao lado dos blocos. A razão está escrita no
código de campo: *"para a telemetria não poder mentir"*. Se o texto enviado e o texto registrado
pudessem divergir — e um cache é exatamente um lugar onde divergem —, então o hash do prompt não
prova mais nada, e a rastreabilidade inteira vira decoração. É o que amarra este standard a
`ai-agents-observability-patterns`.

**(3) Determinístico e puro, portanto testável sem infraestrutura.** Um compositor que lê banco não
é testável em ciclo curto, e um compositor não testado é onde a regra de fase silenciosamente para
de valer.

**A dor que o compositor mata**, medida: montagem de prompt em **três lugares** e o filtro de
allow-list em **três lugares** dentro do mesmo produto. Nenhum dos três está errado isoladamente;
juntos, garantem que uma regra nova entre em dois e esqueça o terceiro.

---

## Allow-list por fase

A poda acontece **em dois estágios, ambos em código**, e a ordem importa:

| Estágio | Pergunta | Efeito |
|---|---|---|
| 1 | Esta tool pertence a **esta fase**? | Poda por fase |
| 2 | Este **tenant/plano** tem esta capacidade? | Poda por capacidade |

Regras que o campo fixou, e que valem repetir porque cada uma custou um bug:

- O mapa de capacidade é **declarativo e puro** — uma tabela, não um `switch` com efeitos.
- **Tool ausente do mapa é disponível.** O default é permitir, e a razão é operacional: um mapa
  incompleto que nega por omissão derruba funcionalidade em produção toda vez que alguém adiciona
  uma tool e esquece a linha.
- **A poda preserva a ordem** da lista original. Ordem de tools é entrada do modelo; embaralhá-la
  entre turnos introduz variância que ninguém pediu.

Cobertura de referência no campo: 52 casos de teste **só** sobre o segundo estágio. Não é excesso —
é o estágio em que um erro é invisível (a tool some, o modelo se adapta, e ninguém percebe até a
reclamação).

---

## Exigir tool no turno

Quando **toda** ação do agente é uma tool, um turno que não chama tool nenhuma é, por construção,
um turno perdido.

**`ChatToolMode.RequireAny` existe.** Medido por reflexão sobre
`Microsoft.Extensions.AI.Abstractions` 10.9.0 em 2026-09-08: `ChatToolMode` expõe `Auto`, `None`,
`RequireAny` (do tipo `RequiredChatToolMode`) e o método `RequireSpecific(string functionName)`. A
restrição de uso é a esperada: é preciso haver ao menos uma tool em `ChatOptions.Tools`.

**Este standard o recomenda como forma default para esse caso** — e para no ponto exato em que uma
recomendação viraria prescrição cega.

### A alternativa não é um defeito, e o critério de saída é parte da regra

A alternativa medida em campo é a **segunda passada**: quando a primeira não chama tool, o código
reenvia com uma mensagem fixa pedindo a ação. É fácil chamar isso de gambiarra; os fatos não
sustentam:

- Tem **7 testes dedicados**.
- Tem um vocabulário fechado de **quatro desfechos**, incluindo `recovered` — ou seja, o caso "a
  segunda passada salvou o turno" é **contado**, não suposto.
- **Preserva o desfecho da primeira passada** quando a segunda falha no transporte, em vez de
  transformar um erro de rede em "o agente não fez nada".
- É chamado **no próprio código** de *"migração futura pendente de métrica"*.

Transformar uma migração que o código deixou aberta de propósito numa regra é adiantar uma decisão
que ninguém mediu. Então o standard escreve o **critério de saída**:

> **Migre para `RequireAny` quando a taxa de `recovered` não pagar o custo da segunda chamada.** Os
> dois números existem: a contagem de `recovered` sobre o total de turnos, e o custo por turno da
> chamada extra (`ai-agents-cost-and-budget`). Se `recovered` é raro, a segunda passada é seguro
> caro; se é frequente, ela está compensando um prompt fraco, e `RequireAny` vai expor isso como
> erro em vez de escondê-lo como recuperação.

**E o que se perde na migração**, para que a decisão seja informada: a recuperação best-effort
desaparece (um turno que hoje se salva passa a falhar), e o **acumulador de uso que soma as duas
passadas** perde a razão de existir — se ele for removido junto, a contabilidade de custo daquele
turno muda de forma, o que precisa ser conferido antes e depois.

---

## Teto de tool calls e isenta

Um agente que pode chamar tools em loop precisa de um teto, e o teto precisa de uma **isenta**.

- **Teto por turno** — um número de voltas de tool calling. Referência de campo: 8.
- **Uma tool isenta**, tipicamente a que fala com o interlocutor: com ela configurada, o loop tolera
  N negações pós-teto antes de encerrar o turno. Sem isenta, bater o teto significa **o agente parar
  de responder no meio**, que é o pior desfecho possível numa conversa.
- **O valor efetivo vem da configuração**, por tenant ou por produto, não de uma constante.
- Bater o teto é **evento observável**, não um `return` silencioso.

Nota de camada: o teto de voltas é **nível de agente** (uma opção do pipeline de invocação de
funções), não uma propriedade das opções de chamada. Confundir os dois faz o teto ser enviado ao
provider, onde ele não significa nada.

---

## Agente auxiliar em sequência

**Etapa fixa do turno roda em sequência, no código. Aninhar um agente numa tool call é decisão
própria, com critério — não o default.**

### A fronteira com `multi-agent-patterns`, escrita para não virar contradição

`ai-agents-multi-agent-patterns` é o dono da composição entre agentes, e ele **recomenda**
Agent-as-Tool (`agent.AsAIFunction()` — API real, medida no pin: `AIAgentExtensions.AsAIFunction`
existe em `Microsoft.Agents.AI` 1.20.0), com teto de **≤ 2 níveis** de aninhamento. Isso **não**
contradiz este arquivo, e a fronteira é uma pergunta só:

| Pergunta | Resposta | Forma |
|---|---|---|
| **Quem decide se o auxiliar roda?** O **modelo**, turno a turno | Agent-as-Tool é o padrão: `AsAIFunction()`, ≤ 2 níveis (`ai-agents-multi-agent-patterns`) | aninhado, deliberadamente |
| O **código** — é etapa fixa do turno, sempre acontece | **sequência no código**, nunca aninhada | o orquestrador é o código |

O caso deste standard é quase sempre o segundo: numa conversa com fases, o auxiliar (persona,
sumarização, extração) é **etapa fixa**, não escolha do modelo. Aninhá-lo paga o preço abaixo sem
comprar a única coisa que o aninhamento oferece — deixar o modelo decidir.

**O preço, concretamente:** o custo do agente interno aparece como custo de uma tool (logo, some da
contabilidade por agente); o teto de tool calls do externo não governa o interno; o timeout do turno
passa a cobrir duas chamadas de LLM em série; e o span do interno pendura-se sob `execute_tool`, onde
nenhum painel de agente procura. Quando a decisão **é** do modelo, esses quatro custos continuam
valendo — a diferença é que aí eles foram escolhidos, e o `decisions.md` diz por quê.

Os dois lados de campo, e é raro ter os dois:

| Lado | O que o acervo mostra |
|---|---|
| **Declarado** | Um projeto escreve o contrato explicitamente: *"nenhum agente chama outro; nesting é anti-padrão nomeado"* |
| **Medido como problema** | Outro projeto roda um agente de persona **dentro** de uma tool call do agente principal — e a persona é etapa **fixa** daquele turno, isto é, o segundo caso da tabela acima |

A composição legítima — quando ela é mesmo necessária — é assunto de
`ai-agents-multi-agent-patterns`, e este standard não a repete: aponta.

---

## Anti-patterns

| Anti-pattern | Por quê é problema | Solução |
|---|---|---|
| Um agente por fase | A transição vira orquestração; o histórico fragmenta | Um agente, fase como dado do turno |
| Fase derivada da memória do modelo | O modelo "esquece" a fase, e o bug não é reproduzível | Derive de estado observável, a cada turno |
| Montagem de prompt em mais de um lugar | Regra nova entra em dois e esquece o terceiro | Um compositor, puro |
| String de prompt guardada em cache ao lado dos blocos | A telemetria passa a poder mentir; o hash não prova nada | Derive sem cache, sempre |
| Allow-list negando por omissão | Tool nova sem linha no mapa derruba funcionalidade em produção | Ausente do mapa = disponível |
| Poda que reordena as tools | Variância de entrada que ninguém pediu | Preserve a ordem |
| Teto de tool calls sem isenta | O agente para de responder no meio da conversa | Isenta configurada, com tolerância pós-teto |
| Teto tratado como opção de chamada | Vai ao provider, onde não significa nada | É opção do pipeline, nível de agente |
| `RequireAny` adotado sem medir `recovered` | Troca uma recuperação medida por uma falha nova | Aplique o critério de saída antes |
| Etapa FIXA do turno aninhada numa tool call | Paga custo, teto, timeout e span quebrados sem comprar nada — ninguém precisava decidir | Sequência, no código |
| Agent-as-Tool adotado sem registrar a decisão, ou além de 2 níveis | Os quatro custos viram surpresa, e o 3º nível é sinal de que devia ser sequência | Decisão no `decisions.md`, teto ≤ 2 níveis (`ai-agents-multi-agent-patterns`) |

---

## Checklist (verifiable by morph-eval)

- [ ] Existe **um** compositor de turno, puro e sem I/O, e nenhuma segunda montagem de prompt
- [ ] O prompt é composto de blocos nomeados, e o texto enviado é derivado sem cache
- [ ] A fase é derivada de estado observável a cada turno
- [ ] A allow-list poda por fase e por capacidade, nessa ordem, preservando a ordem das tools
- [ ] Tool ausente do mapa de capacidade é tratada como **disponível**
- [ ] Há teto de tool calls por turno, com tool isenta, e bater o teto é observável
- [ ] O teto está no nível de agente, não nas opções de chamada
- [ ] Se `RequireAny` foi adotado: a taxa de `recovered` e o custo da segunda chamada estão no
      `decisions.md`. Se não foi: o critério de saída está escrito
- [ ] Nenhuma etapa **fixa** do turno é invocada de dentro de uma tool call. Onde há aninhamento, quem decide é o **modelo**, está registrado no `decisions.md`, e respeita o teto de ≤ 2 níveis de `ai-agents-multi-agent-patterns`

---

## References

- `ai-agents-agent-archetypes` — o catálogo das quatro formas; este é o aprofundamento da primeira
- `ai-agents-multi-agent-patterns` — dono da composição entre agentes; a seção de auxiliar aponta
  para lá em vez de duplicá-lo
- `ai-agents-agent-spec` — o agente como dado, e a regra "nulo = não envia"
- `ai-agents-observability-patterns` — a rastreabilidade que a invariante do compositor torna
  confiável
- `ai-agents-cost-and-budget` — o custo da segunda chamada, que é metade do critério de saída
- `ai-agents-testing-ai` — como exercitar compositor, allow-list e teto sem rede

> **Lacuna declarada, não escondida.** O par compilável deste standard seria um
> `TurnPlanComposer` em `templates/dotnet/ai-kit/src/Morph.AiKit/Agents/`, com o teste de
> concatenação sem cache em `tests/Morph.AiKit.Tests/`. **Ele ainda não existe** — nem no ai-kit
> atual nem no plano que o construiu. Por isso o cabeçalho deste arquivo declara verificação
> documental e **não** carimba `provado por`: prova inexistente é pior que prova ausente.

---

*MORPH-SPEC by Polymorphism Tech — ai-agents/conversational-agent-with-phases.md v1.0 (2026-09-08)*
