# Context Providers — Dynamic context injection

> **Scope:** stacks=["dotnet"]
> **Layer:** 2 (on-keyword)
> **Keywords:** context provider, dynamic context, context injection, AIContextProvider, memory provider, compaction, ordem de escolha, plano por fase
> **Read by Claude in:** implement

**Verified against:** Microsoft.Agents.AI 1.20.0 (ai-pin 2026-09-08) + doc oficial de context providers e de compaction (learn.microsoft.com, `ms.date` 2026-07-30). **Verificação documental** para o desenho — o ai-kit não registra `AIContextProvider`; os nomes de tipo, o pacote de cada extensão e o atributo `[Experimental]` da compaction foram **medidos por reflexão sobre `Microsoft.Agents.AI` 1.20.0** em 2026-09-08. Last-verified: 2026-09-08.

---

## Ordem de escolha — a tool vem primeiro

**A regra, antes de qualquer API:** comece pela **tool**. Só suba para um `AIContextProvider` quando o contexto é **transversal** — memória, compaction, tenant, segurança — *e* o LLM **não pode ter a opção** de não buscá-lo.

```
Preciso que o agente saiba X.
├─ O LLM deve decidir SE precisa de X? ──────────────→ TOOL
├─ X depende do que foi perguntado (similaridade)? ──→ RAG (dentro de uma tool)
├─ X vale para TODO turno e o LLM não pode pular? ───→ AIContextProvider
└─ X muda o MODO do agente (instruções + tools
   permitidas por fase do funil)? ──────────────────→ compositor determinístico em C#
                                                      (nem provider, nem tool)
```

**O caso concreto que a regra evita:** RAG passivo injetado como context provider. O usuário digita "oi" e o agente paga uma busca vetorial + o custo dos trechos no prompt — **em todo turno**, inclusive nos que não precisam de conhecimento nenhum. Como tool, o "oi" não dispara busca alguma. Foi medido em produção: o escopo institucional da base do GHLBrain roda sem `LIMIT`, e passivo isso é custo por turno, não por necessidade.

### O compositor determinístico — a alternativa que não é provider

Plano por fase (instruções + **allow-list de tools** conforme o estado do funil) **não** é caso de provider. É C# puro, montado antes de criar a requisição:

```csharp
// O CÓDIGO decide o modo; o LLM só classifica. A allow-list de tools é a parte
// que um provider não consegue dar: AIContext.Tools ACRESCENTA tools, não remove.
public sealed record OperatorPlan(string Instructions, IReadOnlyList<AITool> Tools);

public static OperatorPlan PlanFor(FunnelStage stage) => stage switch
{
    FunnelStage.Triage    => new(TriagePrompt,    [ClassifyLead, ScheduleCall]),
    FunnelStage.Quoting   => new(QuotingPrompt,   [ClassifyLead, BuildQuote, ScheduleCall]),
    FunnelStage.Closing   => new(ClosingPrompt,   [BuildQuote, SendContract]),
    _                     => new(DefaultPrompt,   [ClassifyLead]),
};

var plan = PlanFor(conversation.Stage);
var options = new ChatOptions { Instructions = plan.Instructions, Tools = [.. plan.Tools] };
```

É a prática do GHLBrain (`OperatorPlan`) e o padrão do canal: **o código decide o modo, o LLM classifica**. Um provider que "sugere" quais tools usar deixa a decisão com o modelo; a allow-list tira a decisão do modelo.

---

## Quando usar Context Provider

Use um `AIContextProvider` quando o agente precisa de **contexto dinâmico computado a cada chamada** — informação que varia por request, sessão ou tenant, sem poluir as instructions estáticas do agente — **e** a regra de ordem acima já descartou tool e compositor.

**Exemplos de uso correto:**
- Data/hora atual injetada nas instructions ("Today is Monday, 2026-05-19")
- Perfil do tenant/usuário logado (nome, plano, preferências de idioma)
- Estado de sistema em tempo real (saldo de conta, status de pedido)
- Contexto de segurança (permissões do usuário para o turno atual)

**Distinção crítica:**

| Mecanismo | Quando usar |
|-----------|-------------|
| **Context Provider** | Contexto **determinístico/computado** — sempre injetado, independe da entrada do usuário |
| **Tool** | O agente **decide chamar** baseado na entrada; retorno é dinâmico mas acionado pelo LLM |
| **RAG** | Busca por **similaridade** em base de conhecimento; o contexto varia com o que foi perguntado |

> Regra prática: se você sabe exatamente *o que* injetar antes de chamar o LLM, use Context Provider.
> Se precisa do LLM para decidir *se* e *como* buscar, use Tool ou RAG.

---

## A API

**Pacote: `Microsoft.Agents.AI` 1.20.0 — e só ele.** Correção medida em 2026-09-08: a extensão `UseAIContextProviders` **não** vem de `Microsoft.Agents.AI.Hosting`; ela é publicada por `Microsoft.Extensions.AI.AIContextProviderChatClientBuilderExtensions`, **que está dentro da assembly `Microsoft.Agents.AI`** (o `using` é `Microsoft.Extensions.AI`, o pacote é `Microsoft.Agents.AI`). A versão anterior deste standard atribuía o método ao pacote errado.

**Tipos e membros — medidos por reflexão sobre as assemblies do pin:**

| Símbolo | Assembly | Nota |
|---|---|---|
| `AIContextProvider` | `Microsoft.Agents.AI.Abstractions` | Classe base **abstrata**. Overrides: `ProvideAIContextAsync(InvokingContext, ct)` e `StoreAIContextAsync(InvokedContext, ct)`, ambos `protected` |
| `AIContext` | `Microsoft.Agents.AI.Abstractions` | **Exatamente três** propriedades: `Instructions`, `Messages`, `Tools` |
| `AIContextProvider.InvokingContext` / `.InvokedContext` | idem | Tipos **aninhados** no provider (`AIContextProvider+InvokingContext`) |
| `ProviderSessionState<TState>` | idem | `StateKey`, `GetOrInitializeState(session)`, `SaveState(session, state)` |
| `AgentRequestMessageSourceType` | idem | Struct com `External`, `ChatHistory`, `AIContextProvider` |
| `ChatClientAgentOptions.AIContextProviders` | `Microsoft.Agents.AI` | `IEnumerable<AIContextProvider>` |
| `UseAIContextProviders(this ChatClientBuilder, params AIContextProvider[])` | `Microsoft.Agents.AI` | Registra **no pipeline de chat** — ver a diferença abaixo |
| `Microsoft.Agents.AI.Compaction.*` | `Microsoft.Agents.AI` | **`[Experimental("MAAI001")]`** — medido no atributo do tipo |

**Método a sobrescrever:**

```csharp
// ProvideAIContextAsync  → chamado ANTES do LLM (before-run hook) — injeta contexto
// StoreAIContextAsync    → chamado APÓS o LLM (after-run hook) — persiste estado se necessário
```

### A invariante de instância — a mesma de `ai-agents-agent-session`

**Um provider, uma instância, todas as sessões.** O provider não pode ter campo de estado por conversa; o que é por sessão vive em `ProviderSessionState<TState>` com o `StateKey` declarado em `StateKeys`. Campo de instância guardando "o tenant atual" é como o contexto de um cliente aparece no prompt de outro — e a falha não aparece em teste de uma sessão só, aparece sob concorrência.

### `AgentRequestMessageSourceType` — como não fechar um loop

Mensagem devolvida em `AIContext.Messages` volta ao agente marcada com a origem `AIContextProvider`; mensagem do usuário é `External`; a que veio do acervo é `ChatHistory`. **Um provider que reinjeta o que ele mesmo produziu no turno anterior monta um loop de crescimento.** Filtre por origem antes de reinjetar.

### Compaction — experimental, e o lugar de registro muda o resultado

`Microsoft.Agents.AI.Compaction.CompactionProvider` é um `AIContextProvider` marcado **`[Experimental("MAAI001")]`** (medido no atributo do tipo em 1.20.0): usá-lo exige `#pragma warning disable MAAI001` ou `<NoWarn>`. Estratégias disponíveis: `SummarizationCompactionStrategy`, `SlidingWindowCompactionStrategy`, `TruncationCompactionStrategy`, `ToolResultCompactionStrategy`, `ContextWindowCompactionStrategy`, `PipelineCompactionStrategy`; gatilhos em `CompactionTriggers` (`MessagesExceed`, `TokensExceed`, `TurnsExceed`, `GroupsExceed`, `HasToolCalls`, `Always`/`Never`, `Any`/`All`).

**Onde registrar importa, e a diferença não é cosmética:**

| Registro | Efeito |
|---|---|
| `chatBuilder.UseAIContextProviders(compaction)` (no `ChatClientBuilder`) | Compacta **só o request em voo**. O acervo persistido fica intacto |
| `ChatClientAgentOptions.AIContextProviders = [compaction]` | **Não roda dentro do loop de tools**, e o resumo produzido **vaza para o histórico persistido** |

Consequência prática: **compaction só faz sentido com histórico local**. Com a conversa no provedor (`store: true`) você compacta um lado e o outro continua inteiro. Ver `ai-agents-agent-session`.

---

## Exemplo C# — context provider que injeta data atual e perfil do tenant

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

// ── 1. Definir o provider ──
public sealed class TenantContextProvider : AIContextProvider
{
    private readonly ITenantService _tenantService;
    private readonly ITenantContext _tenantContext;

    // ITenantContext deve ser implementado via IHttpContextAccessor ou AsyncLocal
    // para resolver o tenant atual em runtime — não registre como instância estática.
    // O AIAgent é singleton, então o provider também é singleton; o tenant corrente
    // é lido de forma ambient (IHttpContextAccessor/AsyncLocal) a cada chamada.
    public TenantContextProvider(ITenantService tenantService, ITenantContext tenantContext)
    {
        _tenantService = tenantService;
        _tenantContext = tenantContext;
    }

    // Sobrescreva: injetado ANTES de cada chamada ao LLM
    protected override async ValueTask<AIContext> ProvideAIContextAsync(
        InvokingContext context,
        CancellationToken cancellationToken = default)
    {
        // Tenant resolvido via serviço injetado (ambient context, ex: IHttpContextAccessor)
        var tenantId = _tenantContext.TenantId ?? "unknown";
        var tenant = await _tenantService.GetTenantAsync(tenantId, cancellationToken);

        var instructions = $"""
            Today is {DateTimeOffset.UtcNow:yyyy-MM-dd HH:mm} UTC.
            Tenant: {tenant.Name} (plan: {tenant.Plan}, language: {tenant.Language}).
            Always respond in {tenant.Language}.
            """;

        return new AIContext
        {
            // Injeta como instrução adicional — não como mensagem do usuário
            Instructions = instructions,
        };
    }

    // Sobrescreva (opcional): chamado APÓS o LLM — use para persistir estado derivado da resposta
    protected override ValueTask StoreAIContextAsync(
        InvokedContext context,
        CancellationToken cancellationToken = default)
        => ValueTask.CompletedTask; // stateless provider — nada a persistir
}

// ── 2. Registrar o agente com o provider no DI ──
// Program.cs
builder.Services.AddSingleton<AIAgent>(sp =>
{
    var registry      = sp.GetRequiredService<ModelRegistry>();
    var tenantSvc     = sp.GetRequiredService<ITenantService>();
    var tenantContext = sp.GetRequiredService<ITenantContext>();

    return registry.GetChatClient("text-default")
        .AsAIAgent(new ChatClientAgentOptions
        {
            ChatOptions = new() { Instructions = "You are a helpful assistant." },
            AIContextProviders = [new TenantContextProvider(tenantSvc, tenantContext)],
        });
});

// ── Alternativa: pipeline de chat (compacta/injeta SÓ o request em voo) ──
// using Microsoft.Extensions.AI;   // a extensão vive na assembly Microsoft.Agents.AI
// IChatClient enriched = chatClient.AsBuilder()
//     .UseAIContextProviders(new TenantContextProvider(tenantSvc, tenantContext))
//     .Build();

// ── 3. Uso normal — provider é invocado automaticamente a cada RunAsync ──
AgentSession session = await agent.CreateSessionAsync();
AgentResponse response = await agent.RunAsync("Quais são meus benefícios do plano?", session);
Console.WriteLine(response.Text);
// O agente já sabe a data, o nome do tenant e o idioma — sem código extra no caller
```

---

## Context Provider vs Tools vs RAG

| Mecanismo | Quem decide chamar | Quando é acionado | Exemplo |
|-----------|-------------------|-------------------|---------|
| **Context Provider** | Framework (sempre) | Antes de cada chamada ao LLM | Data/hora, perfil do tenant |
| **Tool** | O LLM (quando achar necessário) | Quando o LLM decide invocar | Consultar saldo, buscar pedido |
| **RAG** | Framework + similaridade semântica | Antes do LLM, busca por embedding | Documentos de ajuda, base de conhecimento |

> Se você está em dúvida entre Context Provider e Tool:
> - Pergunta: "O agente *sempre* precisa dessa informação nesse contexto?" → Context Provider
> - Pergunta: "O agente *pode ou não* precisar dependendo do que o usuário pediu?" → Tool

---

## Anti-patterns

| Anti-pattern | Problema |
|-------------|----------|
| Context Provider para o que uma tool resolve | Injeta contexto desnecessário a cada chamada; desperdiça tokens |
| Injetar contexto gigante a cada chamada | Aumenta custo e pode esgotar context window; prefira tools para contexto on-demand |
| Provider com side-effects de escrita em `ProvideAIContextAsync` | Hook before-run deve ser read-only; use `StoreAIContextAsync` para escrita |
| Compartilhar estado mutável entre providers sem `ProviderSessionState<T>` | Race condition entre providers; use o helper tipado |
| **Estado por sessão em campo do provider** | Uma instância atende todas as sessões: o contexto de um cliente vaza para outro. Use `ProviderSessionState<TState>` e declare o `StateKey` em `StateKeys` |
| **RAG passivo como provider** | Paga busca vetorial e tokens de trecho em TODO turno, inclusive no "oi". RAG é tool |
| **Provider para escolher tools por fase** | `AIContext.Tools` só ACRESCENTA — não existe allow-list por provider. Use o compositor determinístico (§Ordem de escolha) |
| Reinjetar em `AIContext.Messages` o que o próprio provider produziu no turno anterior | Loop de crescimento. Filtre por `AgentRequestMessageSourceType` |
| Compaction registrada em `ChatClientAgentOptions` esperando que rode no loop de tools | Ali ela não roda no loop, e o resumo vaza para o histórico persistido |
| Usar Context Provider para lógica de negócio complexa | Providers devem ser leves e rápidos; lógica pesada vai em tools |

---

## Checklist (verifiable by morph-eval)

- [ ] O provider herda da classe base abstrata `AIContextProvider` (não implementa uma interface genérica)
- [ ] `ProvideAIContextAsync` é read-only — sem writes a banco ou estado global
- [ ] `StoreAIContextAsync` usado somente quando o provider precisa persistir estado
- [ ] Provider registrado em `ChatClientAgentOptions.AIContextProviders` ou via `.UseAIContextProviders()` — com o registro escolhido pela tabela de §Compaction, não por acaso
- [ ] Contexto injetado é **determinístico** — se depende de busca por similaridade, é RAG (numa tool), não provider
- [ ] A regra de ordem foi aplicada: existe uma frase dizendo por que **não** é uma tool
- [ ] Nenhum campo de instância do provider guarda estado de conversa/tenant
- [ ] Compaction (se usada) tem `MAAI001` suprimido conscientemente e o histórico é local

---

## References

- `ai-agents-setup` — packages e DI base
- `ai-agents-agent-session` — a mesma invariante de instância, e por que compaction pede histórico local
- `ai-agents-rag-custom-pgvector` — busca por similaridade (distinto de context provider)
- `ai-agents-multi-agent-patterns` — composição de agentes
- MAF Context Providers docs: https://learn.microsoft.com/en-us/agent-framework/agents/conversations/context-providers (`ms.date` 2026-07-30)
- MAF Compaction docs: https://learn.microsoft.com/en-us/agent-framework/agents/conversations/compaction (`ms.date` 2026-07-30)
- MAF Agent Pipeline docs: https://learn.microsoft.com/en-us/agent-framework/agents/agent-pipeline

---

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