# Agent Spec — o agente como dado, não como construção espalhada

> **Scope:** stacks=["dotnet"]
> **Layer:** 1 (on-keyword)
> **Keywords:** agent spec, AgentSpec, agent catalog, AgentCatalog, um agente por spec, defaults do agente, quantos agentes
> **Read by Claude in:** implement (ao declarar ou alterar um agente) e plan (ao contar quantos agentes a feature cria)

**Verified against:** Microsoft.Agents.AI 1.20.0 + Microsoft.Extensions.AI 10.9.0 (ai-pin 2026-09-08); provado por `templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs`, `.../Agents/AgentSpecOptions.cs` e `.../Agents/AgentFactory.cs` — o record, o bag de opções e a materialização são exatamente o que o kit compila. Last-verified: 2026-09-08.

---

## O agente como dado

**Este standard possui:** a **forma do record** que descreve um agente, o **catálogo** que os
enumera, e a regra **"nulo = não envia"**. Ele **não** possui: timeout, retry, strict e temperatura
como valores de runtime (`ai-agents-llm-runtime-defaults`), tarifa e teto de custo
(`ai-agents-cost-and-budget`), a origem do texto de `Instructions`
(`ai-agents-prompt-sources`), nem o **schema** do arquivo de registry
(`ai-agents-providers-model-registry`, Layer 0). Os cinco descrevem **um** artefato; a fronteira
está escrita nos cinco para que ninguém precise adivinhar onde um campo mora.

A dor que este standard mata é de **contagem**. Quando o agente é construído inline em cada
call-site, ninguém no projeto consegue responder três perguntas triviais:

1. Quantos agentes existem?
2. Cada um roda com que modelo, e com que esforço de raciocínio?
3. Onde está o default que vale de verdade?

Medição de campo que motiva a regra: num projeto do acervo o agente é construído em **cinco
lugares distintos**, e os defaults **divergem do que roda** — um call-site diz um modelo, o
`appsettings.json` diz outro, e a documentação de arquitetura do próprio projeto ainda registra o
primeiro. No mesmo projeto, acrescentar um campo de spec exige **seis pontos de wiring, cinco dos
quais falham em silêncio**.

A forma que resolve: **um agente, uma spec**. A spec é um `record` imutável, sem comportamento de
execução, que não abre conexão e não lê configuração — o que permite descrevê-lo em teste, em JSON
ou em banco sem arrastar infraestrutura junto.

---

## AgentSpec

Os campos, medidos no record que o kit compila:

| Campo | Tipo | Regra |
|---|---|---|
| `Alias` | `string` **required** | Chave no registry, **nunca** um id de modelo. Vazio ou só espaço é rejeitado **na construção** |
| `Instructions` | `string` **required** | Prompt de sistema. Vazio ou só espaço é rejeitado na construção |
| `Api` | `AgentApi?` | Protocolo desta spec. **Nulo = usa o `api:` do alias**, que é o caso normal |
| `Tools` | `IReadOnlyList<AITool>` | Vazio por padrão — **nunca nulo** |
| `OutputSchema` | `JsonElement?` | Nulo = saída livre |
| `Options` | `AgentSpecOptions` | Bag único de opções. Nunca nulo; o default não envia opção nenhuma |
| `Name` | `string?` | Nome legível (telemetria, logs) |
| `Description` | `string?` | Descrição legível |

**Duas decisões de forma que valem mais que a lista.**

**(1) Validar na construção, não no uso.** `Alias` e `Instructions` rejeitam branco no `init`. Um
alias vazio que passa da construção vira uma falha de resolução ilegível várias camadas adiante,
num stack trace que não menciona o agente.

**(2) A precedência mora num lugar só.** Quando a spec traz `Api` e o alias traz outro, quem vence?
A resposta é um método no próprio record — `ResolveApi(aliasApi) => Api ?? aliasApi` — e **não** uma
condicional dentro da fábrica. O motivo é escrito no kit: divergência silenciosa entre spec e
registry é exatamente o bug que o `api:` por alias existe para eliminar, e uma regra copiada em dois
lugares é uma regra que já divergiu.

### A regra que este standard torna doutrina: nulo = não envia

**Propriedade nula significa "não envia ao provider" — nunca "envia o default do framework".**

Sem essa regra, cada propriedade opcional carrega uma segunda pergunta invisível: *o framework
preenche isso por mim?* Com ela, a leitura é local e total: o que está nulo não aparece na chamada,
e o que aparece na chamada é o que alguém escreveu.

O corolário incômodo, e ele é parte da regra: **dentro de `Options`, lista vazia não é tratada como
nulo**. `[]` é enviado como veio — `StopSequences` é o exemplar: o kit testa `is not null`, não
`Count > 0`.

**E há exatamente uma exceção, que este standard nomeia em vez de esconder.** `Tools` não vive em
`Options`: vive na própria `AgentSpec`, é `IReadOnlyList<AITool>` (nunca nulo, vazio por padrão), e o
kit a copia sob `if (spec.Tools.Count > 0)` — medido em `AgentFactory.cs`. Consequência: um
`Tools = []` **explícito** não chega ao `ChatOptions`, e o provider recebe a chamada sem lista de
tools em vez de com uma lista vazia.

Isso é defensável (uma lista vazia enviada muda o comportamento de alguns providers) e é **por isso**
que precisa estar escrito: a regra "nulo não envia; não-nulo envia o que você escreveu" vale
**dentro de `Options`**; em `Tools`, o vazio também não envia, porque ali não existe nulo para
distinguir do vazio. Uma exceção nomeada é uma regra; uma exceção não nomeada é o tipo de coisa que
só se descobre lendo a fatura.

O segundo corolário: quando `Options` não traz o esforço de raciocínio, vale **o default do
provider**, e o kit **não afirma saber qual é**. Não saber e dizer que não sabe é mais barato do que
documentar um default que o provider muda sem avisar.

---

## AgentCatalog

Um lugar que responde: *quantos agentes existem, com que alias, e com que opções*. É o que
transforma "cinco call-sites" em "cinco linhas de uma lista".

O catálogo é **enumerável** e **inerte**: ele não constrói agentes, ele os declara. Construir é da
fábrica.

> **Item de catálogo sem consumidor em runtime é letra morta.** Precedente medido: um agente
> seedado por migration, com prompt versionado no banco, **sem nenhum consumidor em C#** —
> aparecendo apenas como alias fictício num arquivo de teste. Ele custou uma migration, ocupa uma
> linha na tabela de prompts e responde por zero comportamento.

Por isso a regra é dupla: todo agente do catálogo **é referenciado por um caminho de execução**, e
todo agente executado **está no catálogo**. Uma verificação barata é contar: quantidade de entradas
no catálogo × quantidade de call-sites que resolvem uma entrada. Divergência é achado, não ruído.

---

## Defaults fora do código

O default do agente mora **na configuração do registry**, não num literal. Um valor default escrito
em C# é invisível para quem lê o JSON, e um valor no JSON é invisível para quem lê o C# — ter os
dois é garantir que um deles esteja errado.

Regras:

- O alias default é declarado no documento do registry (`defaultAlias`), não escolhido por um `??`
  em algum handler.
- Um limite conhecido de um alias (por exemplo, "este modelo rejeita tool calling com esforço de
  raciocínio diferente de `None`") é **campo do alias, com procedência escrita**, nunca uma regra
  universal chumbada no código. O kit recusa carregar um limite sem procedência — justamente para
  que ninguém consiga gravar um "todo mundo sabe que" sem dizer de onde tirou.
- Quando um default do provider é desconhecido, o correto é **não enviar** e não documentar palpite.

---

## Higiene de publicação

O documento do registry é **arquivo de produção**. Override de desenvolvimento não mora nele.

Os dois lados foram medidos no acervo:

| Prática | O que aconteceu |
|---|---|
| **Anti-padrão** | Um alias apontando para um modelo local foi **commitado** no registry, com o comentário `"OVERRIDE LOCAL (teste com Ollama) — NÃO COMMITAR"` ao lado. O comentário estava certo; o commit aconteceu do mesmo jeito |
| **Padrão** | Outro projeto mantém deliberadamente o override de endpoint e de modelo **fora** do registry, em configuração de ambiente, com a razão escrita no código: *"o JSON também vale em produção"* |

A regra: o que vale nas duas pontas vai no registry; o que vale só na sua máquina vai em
configuração de ambiente ou em `appsettings.Development.json`, que não é publicado. Um comentário
avisando para não commitar não é um mecanismo — é uma esperança.

---

## Anti-patterns

| Anti-pattern | Por quê é problema | Solução |
|---|---|---|
| Agente construído inline em cada call-site | Ninguém consegue responder "quantos agentes existem" nem "com que modelo" | Uma `AgentSpec` por agente, num catálogo |
| Id de modelo dentro da spec | Trocar de provider vira uma varredura; o registry deixa de ser fonte | `Alias`, sempre |
| Default do agente escrito como literal em C# | Duas fontes para o mesmo valor, e uma delas está errada | Default no documento do registry |
| Nulo interpretado como "manda o default do framework" | A chamada passa a carregar valores que ninguém escreveu | Nulo = não envia. Sem exceção |
| Lista vazia tratada como nula **sem a exceção nomeada** | A regra deixa de ser local: quem lê não sabe que `Tools = []` não chega ao provider | `[]` é enviado como veio dentro de `Options`; a exceção de `Tools` fica escrita |
| Precedência spec × alias resolvida dentro da fábrica | A regra copiada em dois lugares já divergiu | Um método no próprio record |
| Entrada de catálogo sem consumidor em runtime | Migration paga, linha na tabela, zero comportamento | Toda entrada é referenciada por um caminho de execução |
| Override de dev commitado no registry | O arquivo vale em produção; o override vaza | Override fora do arquivo publicado |
| Validação de `Alias`/`Instructions` adiada para o uso | Falha ilegível várias camadas adiante | Rejeite branco no `init` |

---

## Checklist (verifiable by morph-eval)

- [ ] Existe exatamente **uma** `AgentSpec` por agente, e nenhum agente é construído inline
- [ ] Nenhuma spec contém id de modelo — só `Alias`
- [ ] `Alias` e `Instructions` são validados na construção (branco é rejeitado ali)
- [ ] A precedência entre o `api:` da spec e o do alias existe em **um** lugar
- [ ] Toda propriedade opcional nula **não** é enviada ao provider; nenhum código preenche default
      de framework por baixo
- [ ] Toda exceção à regra "não-nulo envia o que você escreveu" está **nomeada** no standard do
      projeto (no exemplar do kit há exatamente uma: `Tools = []` não é enviada)
- [ ] O catálogo enumera todos os agentes, e toda entrada tem ao menos um consumidor em runtime
- [ ] O `defaultAlias` está no documento do registry, não num `??` de handler
- [ ] Nenhum override de desenvolvimento no arquivo de registry publicado

---

## References

- `ai-agents-providers-model-registry` — Layer 0, dono do **schema** do documento, do `defaultAlias`,
  do fallback e da tabela de providers. Este standard não repete nada disso
- `ai-agents-setup` — pacotes e `Program.cs`; a construção provider a provider é lá
- `ai-agents-agent-archetypes` — qual **forma** de agente a spec descreve
- `ai-agents-llm-runtime-defaults` — timeout, retry, strict, temperatura e o pin de SDK
- `ai-agents-cost-and-budget` — `pricing` por alias, cálculo e teto
- `ai-agents-prompt-sources` — de onde vem o texto de `Instructions`, e como provar qual versão rodou

---

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