Context providers run around every agent invocation. Before the LLM is called, a provider can inject extra instructions, messages, or tools. After the LLM responds, it can store anything relevant (facts, memory IDs, etc.) in the session. The same pattern works for any kind of dynamic context: current date, user preferences, retrieved documents, and more.
How It Fits Together
| Phase | Method to override | Purpose |
|---|
| Before LLM call | ProvideAIContextAsync | Return additional instructions, messages, or tools to append to the request. |
| After LLM call | StoreAIContextAsync | Extract and persist relevant data from the request/response messages. |
Providers are registered in ChatClientAgentOptions.AIContextProviders and run for every RunAsync() call on the agent.
Demo 1: Built-in Registration Pattern
Register providers when creating the agent via ChatClientAgentOptions. The example below adds a DateTimeContextProvider that tells the agent the current date before every turn - without the caller having to do anything special.
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
AIAgent agent = new OpenAIClient("<your_api_key>")
.GetChatClient("gpt-4o-mini")
.AsIChatClient()
.AsAIAgent(new ChatClientAgentOptions
{
Name = "Assistant",
ChatOptions = new() { Instructions = "You are a helpful assistant." },
AIContextProviders = [new DateTimeContextProvider()],
});
AgentSession session = await agent.CreateSessionAsync();
// The agent knows today's date because the provider injects it automatically.
AgentResponse response = await agent.RunAsync("What is today's date?", session);
Console.WriteLine(response.Text);
The provider itself is minimal - override ProvideAIContextAsync and return an AIContext with the extra information:
internal sealed class DateTimeContextProvider : AIContextProvider
{
// base(null, null) uses the default message filters.
public DateTimeContextProvider() : base(null, null) { }
// Every provider must have a unique state key (even if no state is stored).
public override string StateKey => nameof(DateTimeContextProvider);
// Called BEFORE the LLM - return extra instructions/messages/tools here.
protected override ValueTask<AIContext> ProvideAIContextAsync(
InvokingContext context, CancellationToken cancellationToken = default)
{
return ValueTask.FromResult(new AIContext
{
Instructions = $"Today's date (UTC) is: {DateTime.UtcNow:D}.",
});
}
// Called AFTER the LLM - nothing to store for this provider.
protected override ValueTask StoreAIContextAsync(
InvokedContext context, CancellationToken cancellationToken = default)
=> ValueTask.CompletedTask;
}
Demo 2: Simple Custom Provider with Session State
Because the same provider instance is shared across all sessions, any data that is specific to one conversation must be stored in the AgentSession itself. The helper class ProviderSessionState<T> handles this automatically.
The example below builds a NotesContextProvider:
- StoreAIContextAsync - copies each user message into a per-session notes list.
- ProvideAIContextAsync - prepends the stored notes as an extra message so the agent always has them in view.
internal sealed class NotesContextProvider : AIContextProvider
{
private readonly ProviderSessionState<NotesState> _sessionState;
public NotesContextProvider() : base(null, null)
{
_sessionState = new ProviderSessionState<NotesState>(
stateInitializer: _ => new NotesState(),
stateKey: nameof(NotesContextProvider));
}
public override string StateKey => _sessionState.StateKey;
// Called BEFORE the LLM - inject stored notes as an extra message.
protected override ValueTask<AIContext> ProvideAIContextAsync(
InvokingContext context, CancellationToken cancellationToken = default)
{
var state = _sessionState.GetOrInitializeState(context.Session);
if (state.Notes.Count == 0)
return ValueTask.FromResult(new AIContext());
string notesSummary = "Notes from previous turns:\n"
+ string.Join("\n", state.Notes.Select((n, i) => $" {i + 1}. {n}"));
return ValueTask.FromResult(new AIContext
{
Messages = [new ChatMessage(ChatRole.User, notesSummary)],
});
}
// Called AFTER the LLM - save user messages into session state.
protected override ValueTask StoreAIContextAsync(
InvokedContext context, CancellationToken cancellationToken = default)
{
var state = _sessionState.GetOrInitializeState(context.Session);
foreach (ChatMessage msg in context.RequestMessages.Where(m => m.Role == ChatRole.User))
{
string text = string.Join("", msg.Contents.OfType<TextContent>().Select(t => t.Text));
if (!string.IsNullOrWhiteSpace(text))
state.Notes.Add(text);
}
_sessionState.SaveState(context.Session, state);
return ValueTask.CompletedTask;
}
public class NotesState
{
public List<string> Notes { get; set; } = [];
}
}
Registration is the same as Demo 1 - add new NotesContextProvider() to AIContextProviders:
AIAgent agent = new OpenAIClient("<your_api_key>")
.GetChatClient("gpt-4o-mini")
.AsIChatClient()
.AsAIAgent(new ChatClientAgentOptions
{
Name = "Assistant",
ChatOptions = new() { Instructions = "You are a helpful assistant." },
AIContextProviders = [new NotesContextProvider()],
});
AgentSession session = await agent.CreateSessionAsync();
AgentResponse r1 = await agent.RunAsync("My favourite colour is blue.", session);
Console.WriteLine(r1.Text);
// Turn 2: the provider injects the note from turn 1.
AgentResponse r2 = await agent.RunAsync("What is my favourite colour?", session);
Console.WriteLine(r2.Text); // Agent recalls "blue"
Important: Session State, Not Provider Fields
A provider instance is reused for every session on the agent. Never store conversation-specific data in a provider field - two concurrent sessions would overwrite each other. Always use ProviderSessionState<T> to keep data isolated per session.
| Correct | Wrong |
|---|
Store data via _sessionState.SaveState(session, ...) | Store data in a private List<string> _notes = [] field |
Summary
| Concept | API | Description |
|---|
| Register providers | ChatClientAgentOptions.AIContextProviders | List of providers that run around every RunAsync call. |
| Inject context before LLM | ProvideAIContextAsync(InvokingContext) | Return AIContext with extra instructions, messages, or tools. |
| Store data after LLM | StoreAIContextAsync(InvokedContext) | Persist relevant data from request/response messages. |
| Per-session storage | ProviderSessionState<T> | Stores typed state in the AgentSession, isolated per conversation. |
| Unique provider key | override string StateKey | Must be unique across all providers on the same agent. |
Required Packages
| Package | Purpose |
|---|
Microsoft.Agents.AI | AIAgent, AIContextProvider, ProviderSessionState, AIContext |
Microsoft.Extensions.AI.OpenAI | AsIChatClient() and OpenAI integration |
References
- Microsoft Agent Framework - Context Providers
Link Interceptor Active