AgentSession and Multi-Turn Conversations in Microsoft Agent Framework
One of the defining capabilities of a real AI agent is memory -- the ability to refer back to what was said earlier in a conversation and respond coherently across multiple turns. Without it, every prompt is isolated and the "agent" is really just a fancy function call. Microsoft agent framework session management is the mechanism that changes that: AgentSession is the lightweight container that carries conversation history across calls, turning stateless RunAsync invocations into coherent multi-turn exchanges.
This article covers everything about AgentSession in the Microsoft Agent Framework -- what it is, why you need it, how to pass it to RunAsync, how session state works under the hood, session lifecycle and disposal, concurrent session management, and a complete practical example of a multi-turn Q&A loop.
MAF is in public preview at version 1.0.0-rc1. APIs described here reflect the current state and may change before GA.
What Is AgentSession?
AgentSession is MAF's conversation state container. It accumulates the message history for a single conversation thread and makes that history available to the agent on every subsequent call. Without a session, each RunAsync call is stateless -- the agent has no knowledge of previous exchanges. With a session, each call builds on the previous ones.
The session represents the "memory" of a single conversation. It is not tied to a specific user identity, device, or channel -- it is simply a container for a message list. You decide when to create it, how long to keep it, and when to discard it.
Session objects are lightweight. Creating one has negligible overhead. You can create thousands of sessions without any meaningful resource pressure. The only state they hold is the message history, which grows as the conversation progresses.
Why You Need Session for Multi-Turn Conversations
Without a session, every call to RunAsync starts fresh:
using Microsoft.Agents.AI;
IAIAgent agent = /* ... */;
// First call -- no session
AgentResponse r1 = await agent.RunAsync("My name is Alice.");
Console.WriteLine(r1.Text); // "Hello, Alice! How can I help you?"
// Second call -- no session, agent has no memory of Alice
AgentResponse r2 = await agent.RunAsync("What is my name?");
Console.WriteLine(r2.Text); // "I don't know your name -- you haven't told me yet."
The agent cannot remember Alice's name because there is no persistent conversation history between the two calls. Each call is a brand new conversation from the model's perspective.
Add a session and the context carries forward:
var session = await agent.CreateSessionAsync();
AgentResponse r1 = await agent.RunAsync("My name is Alice.", session);
Console.WriteLine(r1.Text); // "Hello, Alice! How can I help you?"
AgentResponse r2 = await agent.RunAsync("What is my name?", session);
Console.WriteLine(r2.Text); // "Your name is Alice."
The session accumulates the exchange after each call. When you pass it to the next RunAsync, the full conversation history is included in the LLM request. The model sees the complete context and responds accordingly.
How Session State Works
Internally, AgentSession maintains an ordered list of messages. Each RunAsync call:
- Reads the current message history from the session
- Appends the new user prompt
- Sends the full message list (system prompt + history + new prompt) to the LLM
- Receives the response
- Appends the assistant's response to the session
- Returns the
AgentResponse
The system prompt (your agent instructions) is prepended automatically by ChatClientAgent on every call. It is not stored in the session -- it is always injected fresh. This means you can reuse the same session with a different agent without the previous agent's instructions polluting the history.
The message list grows with each turn. A session that has been active for ten exchanges contains twenty messages (ten user + ten assistant). Be aware that very long sessions eventually hit the model's context window limit. For very long conversations, you may need to implement a summarization or trimming strategy.
Session Lifecycle: Create, Use, Dispose
The session lifecycle follows a simple pattern: create it at the start of a conversation, pass it to every RunAsync call in that conversation, and dispose it when the conversation ends.
using Microsoft.Agents.AI;
IAIAgent agent = /* from DI */;
// Start a new conversation
var session = await agent.CreateSessionAsync();
try
{
string[] userTurns =
[
"I'm building a REST API in .NET 10. Can you help me?",
"What's the best way to structure my controllers?",
"Should I use minimal APIs or MVC-style controllers for this?",
"How do I add request validation?",
"Thanks, that covers everything I needed."
];
foreach (var prompt in userTurns)
{
Console.WriteLine($"User: {prompt}");
AgentResponse response = await agent.RunAsync(prompt, session);
Console.WriteLine($"Agent: {response.Text}");
Console.WriteLine();
}
}
finally
{
// Dispose when the conversation ends
if (session is IDisposable disposable)
{
disposable.Dispose();
}
}
If AgentSession implements IDisposable or IAsyncDisposable, use a using statement or using declaration for cleaner code:
var session = await agent.CreateSessionAsync();
AgentResponse r1 = await agent.RunAsync("Hello, I need help with C# generics.", session);
Console.WriteLine(r1.Text);
AgentResponse r2 = await agent.RunAsync("Can you give me a generic repository example?", session);
Console.WriteLine(r2.Text);
When the session goes out of scope, it is cleaned up automatically. Do not share a session across unrelated conversations -- each conversation should have its own session instance.
Concurrent Sessions
A single IAIAgent instance handles multiple concurrent sessions safely. Each session is independent -- there is no shared mutable state between sessions at the agent level. You can have thousands of concurrent active sessions against the same agent.
This is exactly the pattern you'd use in a web application where each user has an ongoing chat:
using Microsoft.Agents.AI;
using Microsoft.Extensions.Caching.Memory;
// In-memory session store (replace with distributed cache for multi-instance apps)
public sealed class SessionStore
{
private readonly IMemoryCache _cache;
public SessionStore(IMemoryCache cache)
{
_cache = cache;
}
public async Task<AgentSession> GetOrCreateAsync(string conversationId, IAIAgent agent)
{
if (!_cache.TryGetValue(conversationId, out AgentSession? session))
{
session = await agent.CreateSessionAsync();
_cache.Set(conversationId, session, new MemoryCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(30)
});
}
return session!;
}
public void Remove(string conversationId)
{
_cache.Remove(conversationId);
}
}
Each user or conversation ID maps to its own AgentSession. The IAIAgent singleton handles all of them concurrently. Sessions expire automatically via the cache sliding expiration, which frees memory for inactive conversations.
For multi-instance deployments, replace IMemoryCache with a distributed cache (Redis, for example) -- though this requires that AgentSession can be serialized and deserialized, which depends on MAF's API at the time of your implementation.
Thread Safety Considerations
An AgentSession should not be shared across concurrent calls for the same conversation. If two requests for the same conversation execute simultaneously, both reads and writes to the session's message list can produce inconsistent results.
The correct pattern for web applications is to serialize calls per conversation. Use a session-scoped lock or a queue per conversation ID if your application allows parallel requests within the same session:
public sealed class LockedSessionAgent
{
private readonly IAIAgent _agent;
private readonly Dictionary<string, SemaphoreSlim> _locks = new();
private readonly Dictionary<string, AgentSession> _sessions = new();
public LockedSessionAgent(IAIAgent agent)
{
_agent = agent;
}
public async Task<AgentResponse> ChatAsync(string conversationId, string prompt)
{
var semaphore = _locks.GetOrAdd(conversationId, _ => new SemaphoreSlim(1, 1));
await semaphore.WaitAsync();
try
{
if (!_sessions.TryGetValue(conversationId, out var session))
{
session = await _agent.CreateSessionAsync();
_sessions[conversationId] = session;
}
return await _agent.RunAsync(prompt, session);
}
finally
{
semaphore.Release();
}
}
}
For most applications, this level of locking is not needed -- users naturally produce sequential messages in a chat interface. But if your application routes messages from the same conversation in parallel, protect the session accordingly. The dependency injection patterns you already use apply here: register LockedSessionAgent as a singleton in the DI container.
Building a Multi-Turn Q&A Loop
Here's a complete interactive console application that demonstrates a multi-turn Q&A loop using AgentSession:
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
IChatClient chatClient = new OpenAIClient(new ApiKeyCredential(apiKey))
.GetChatClient("gpt-4o-mini")
.AsIChatClient();
IAIAgent agent = chatClient.AsAIAgent(
instructions: """
You are a knowledgeable C# and .NET assistant.
Remember context from earlier in the conversation.
When the user says "bye" or "exit", give a brief farewell.
""");
Console.WriteLine("C# Assistant (type 'exit' to quit)");
Console.WriteLine("=====================================");
var session = await agent.CreateSessionAsync();
while (true)
{
Console.Write("\nYou: ");
var input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
continue;
if (input.Trim().Equals("exit", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Ending session...");
break;
}
Console.Write("Agent: ");
// Stream the response for a better user experience
await foreach (StreamingAgentResponse chunk in agent.RunStreamingAsync(input, session))
{
Console.Write(chunk.Text);
}
Console.WriteLine();
}
Console.WriteLine("Session ended.");
Run this and you'll have a fully stateful conversation loop. The agent remembers everything you've said within the session. Try introducing yourself, then asking later questions that reference earlier context -- the agent handles them naturally because the full history is passed with each call.
Sessions in ASP.NET Core Web API
In a web application, you'll typically manage sessions at the request level, associating them with conversation IDs from the client. Here's a minimal ASP.NET Core endpoint pattern:
using Microsoft.Agents.AI;
app.MapPost("/chat/{conversationId}", async (
string conversationId,
ChatRequest request,
SessionStore sessionStore,
IAIAgent agent) =>
{
var session = await sessionStore.GetOrCreateAsync(conversationId, agent);
var response = await agent.RunAsync(request.Prompt, session);
return Results.Ok(new { reply = response.Text });
});
record ChatRequest(string Prompt);
The session is retrieved or created based on the conversation ID. The agent processes the prompt with the session's full history. The next request for the same conversationId retrieves the same session and continues where it left off. This pattern integrates naturally with ASP.NET Core's DI and request pipeline.
For a plugin architecture where different agents handle different conversation types, the session store can be shared -- each session carries history that is meaningful to the agent that created it.
Managing Session Length
As conversations grow, the message history passed to the LLM grows proportionally. Every token in the history costs tokens in the context window. For very long conversations, you'll eventually hit limits.
Strategies to manage session length:
- Summarize periodically -- after every N turns, ask the agent to summarize the conversation so far. Replace the history with the summary plus the last few turns.
- Trim old turns -- keep only the last N message pairs. Older history is less relevant for most conversations.
- Topic-based segmentation -- when the user changes topics significantly, start a new session. The previous session can be archived if needed.
- Use a session factory -- create a builder-style session factory that configures max message count and applies trimming automatically.
The right strategy depends on your use case. Customer support agents may benefit from full history for accountability. Coding assistants may only need the last few exchanges.
FAQ
What is AgentSession in Microsoft Agent Framework?
AgentSession is the conversation history container in MAF. It accumulates user and assistant messages across multiple RunAsync or RunStreamingAsync calls. Pass the same session instance to every call in a conversation to maintain context. Without a session, each call is stateless and the agent has no memory of previous exchanges.
Does AgentSession persist between application restarts?
Not by default. AgentSession is an in-memory object. If your application restarts, the session's message history is lost. For durable conversations, you'd need to serialize the session state to a persistent store (database, Redis, blob storage) and reconstruct it on each request. MAF's current public preview API may add serialization support before GA.
Can I use the same AgentSession with multiple agents?
Yes, technically -- a session is just a message container. But be careful: different agents have different system prompts (instructions), and mixing agents in the same session can produce confusing context. The general recommendation is one agent per session for predictable behavior.
How many concurrent sessions can one agent handle?
As many as your infrastructure supports. IAIAgent instances are stateless with respect to individual sessions. The limiting factors are network connections to the LLM provider, API rate limits, and memory for storing session objects. A single agent instance can handle thousands of concurrent sessions.
What happens if I pass null for the session parameter?
If you pass null (or omit the session parameter), RunAsync treats the call as stateless -- no conversation history is included except the system prompt. Each call stands alone. This is the correct behavior for single-turn interactions where history is not needed.
Should I create a new session or reuse an existing one for a returning user?
It depends on your application's memory requirements. For a short-session chat (current conversation only), create a new session per chat thread and discard it when done. For persistent memory across sessions (the agent remembers the user from previous visits), you'd need to load and reconstruct session state from a persistent store at the start of each new session.
How do I end a session?
Dispose the AgentSession when the conversation ends -- or simply let it go out of scope if it doesn't implement IDisposable. Once you stop passing the session to RunAsync, the history accumulation stops. You can also explicitly clear or remove the session from your session store to free memory.
