BrandGhost
Managing Sessions and Context in GitHub Copilot SDK for C#: Patterns and Best Practices

Managing Sessions and Context in GitHub Copilot SDK for C#: Patterns and Best Practices

If you've been building AI-powered applications with the GitHub Copilot SDK for C#, you've probably realized that managing sessions and context is what separates a basic chatbot from a genuinely intelligent assistant. When it comes to managing sessions and context in github copilot sdk for c#, when you're managing sessions GitHub Copilot SDK C# applications, context is the memory that allows your AI to understand what the user said three messages ago, recall their preferences, and maintain a coherent conversation flow. In this article, I'm going to walk you through the patterns and best practices for managing sessions GitHub Copilot SDK C# applications need to handle multi-turn conversations, context accumulation, and production-ready session lifecycle management. Whether you're building a simple Q&A bot or a complex agentic system, understanding these patterns will help you build more capable and reliable .NET applications.

How Sessions Maintain Context When Managing Sessions GitHub Copilot SDK C#

When you work with the GitHub Copilot SDK, sessions are the fundamental unit that holds conversation history. Every time you send a message through a CopilotSession, that message and the assistant's response become part of the session's internal context. This means the next message you send can reference what was said before -- the LLM sees the entire conversation history, not just the latest prompt.

This design has profound implications for how you structure your application when managing sessions GitHub Copilot SDK C# implementations. Unlike stateless HTTP APIs where each request stands alone, a Copilot session is inherently stateful. The session object maintains this state in memory, and as long as the session remains alive, the conversation context persists. This is why you can ask a follow-up question like "what about performance?" and the model knows you're still talking about the topic from three messages ago.

For .NET developers, this means you need to think about session lifecycle management similar to how you'd manage database connections or HTTP clients. Sessions are resources that need proper initialization, usage, and disposal. The difference is that sessions carry conversational weight -- their value accumulates over time, making them more precious than a simple connection handle.

Session-Per-Conversation Pattern

The most common and straightforward pattern is the session-per-conversation approach. In this pattern, you create one CopilotSession for each distinct user conversation, use it for the duration of that conversation, and dispose of it when the conversation ends. This pattern maps naturally to user interactions like a chat window, a support ticket, or a single code review session.

Here's how I typically implement this pattern in a service class:

public class ConversationService : IAsyncDisposable
{
    private readonly CopilotClient _client;
    private CopilotSession? _session;
    
    public ConversationService(CopilotClient client)
    {
        _client = client;
    }
    
    public async Task InitializeAsync()
    {
        _session = await _client.CreateSessionAsync(new SessionConfig
        {
            Model = "gpt-5",
            Streaming = true
        });
    }
    
    public async Task<string> SendAsync(string userMessage)
    {
        if (_session is null) 
            throw new InvalidOperationException("Call InitializeAsync first");
        
        var tcs = new TaskCompletionSource<string>();
        var sb = new System.Text.StringBuilder();
        
        _session.On(evt =>
        {
            switch (evt)
            {
                case AssistantMessageEvent msg: sb.Append(msg.Data.Content); break;
                case SessionIdleEvent: tcs.TrySetResult(sb.ToString()); break;
                case SessionErrorEvent err: tcs.TrySetException(new Exception(err.Data.Message)); break;
            }
        });
        
        await _session.SendAsync(new MessageOptions { Prompt = userMessage });
        return await tcs.Task;
    }
    
    public async ValueTask DisposeAsync()
    {
        if (_session is not null)
            await _session.DisposeAsync();
    }
}

This pattern gives you clear boundaries. When a user opens a chat window, you initialize the service. Each message they send goes through the same session. When they close the window or the conversation times out, you dispose of the service and the session with it. The context lives exactly as long as the conversation does, which is usually what you want.

You can register this as a scoped service in your IServiceCollection dependency injection configuration, tying the session lifetime to a web request scope or a SignalR connection scope. This gives you automatic cleanup when the scope ends.

Context Accumulation Over Time

One of the most important aspects of managing sessions GitHub Copilot SDK C# developers need to understand is that context accumulates with every turn. Each user message and assistant response adds tokens to the conversation history, and that history is sent to the LLM with every subsequent request. This is how the model "remembers" what you've been talking about, but it also means the context window fills up over time.

Most LLMs have a maximum context window measured in tokens. For example, GPT-5.2 supports 400,000 tokens (400k). In a typical conversation, each exchange might consume 200-500 tokens depending on message length. This means you could theoretically have 800-2000 exchanges before hitting the limit, but in practice, you'll start seeing performance degradation well before that.

Source: Azure OpenAI Service models - GPT-5.2

Here's what context accumulation looks like over multiple turns:

// First turn
await session.SendAsync(new MessageOptions 
{ 
    Prompt = "What is dependency injection in C#?" 
});
// Context now contains: [user message 1, assistant response 1]

// Second turn - builds on first
await session.SendAsync(new MessageOptions 
{ 
    Prompt = "Can you show me an example using IServiceCollection?" 
});
// Context now contains: [user 1, assistant 1, user 2, assistant 2]
// The model sees "IServiceCollection" refers back to dependency injection

// Third turn - builds on both previous
await session.SendAsync(new MessageOptions 
{ 
    Prompt = "How does that example handle singleton vs transient lifetimes?" 
});
// Context now contains: [user 1, assistant 1, user 2, assistant 2, user 3, assistant 3]
// The model sees "that example" refers to the IServiceCollection code from turn 2

As conversations grow longer, you need to be mindful of this accumulation. Some applications benefit from long context -- an AI pair programmer that remembers an entire coding session, for instance. Others may need context management strategies to prevent unbounded growth.

Resetting Context: When and How

There are times when you want the LLM to "forget" and start fresh. Maybe the user has moved on to a completely different topic, or the conversation has become confused and contradictory. Resetting context gives you a clean slate.

The most straightforward way to reset context is to create a new session. When you dispose of the old CopilotSession and create a fresh one, all conversation history is discarded:

public async Task ResetConversationAsync()
{
    if (_session is not null)
    {
        await _session.DisposeAsync();
    }
    
    _session = await _client.CreateSessionAsync(new SessionConfig
    {
        Model = "gpt-5",
        Streaming = true
    });
}

This is a hard reset -- the model has no memory of what came before. Sometimes you want a softer reset where you clear the conversation but maintain certain persistent instructions. You can achieve this by injecting a new system prompt after creating the fresh session, effectively giving the model a new starting context without the baggage of the old conversation.

There's also strategic value in knowing when not to reset. If a user is debugging a complex problem and mentions "that error from before," you want the session to remember. But if they say "let's talk about something completely different," that's a signal that a reset might improve response quality. Some applications let users explicitly reset ("clear chat history"), while others do it automatically based on time gaps or topic detection.

System Message Configuration

One of the most powerful techniques for managing sessions GitHub Copilot SDK C# applications can use is configuring a system message at the session level. This is where you inject initial context at the very start of a session to prime the conversation with persistent instructions, user preferences, or domain-specific knowledge.

The GitHub Copilot SDK provides the SystemMessage property in SessionConfig that allows you to set persistent system instructions:

public async Task InitializeWithSystemPromptAsync(string userRole)
{
    _session = await _client.CreateSessionAsync(new SessionConfig
    {
        Model = "gpt-5",
        Streaming = true,
        SystemMessage = new SystemMessageConfig
        {
            Mode = SystemMessageMode.Append,  // or SystemMessageMode.Replace
            Content = $"""
                You are an AI assistant helping a {userRole}.
                
                Guidelines:
                - Provide C# code examples using .NET 9 features when relevant
                - Assume the user is familiar with SOLID principles
                - Keep responses concise but thorough
                - Always validate assumptions before making recommendations
                """
        }
    });
}

This pattern is incredibly useful for building specialized assistants. If you're building a .NET code review bot, you can inject coding standards and architectural patterns into the system message. If you're building a customer support assistant, you can inject product documentation and company policies. The key is that these instructions persist throughout the session -- every subsequent message the model generates will be influenced by that system context. The SystemMessageMode.Append mode adds your content to any existing system message, while SystemMessageMode.Replace overwrites it entirely.

Source: GitHub Copilot SDK .NET README

I've found this especially valuable when combined with the facade pattern for simplifying complex subsystems -- you can hide the system message configuration behind a clean API that just looks like "create a session" to your application code.

Session Pooling for High-Traffic Apps

When you're building a high-traffic application and managing sessions GitHub Copilot SDK C# apps at scale, creating a new session for every user interaction can become a performance bottleneck. Session creation involves network round trips and resource allocation. This is where session pooling comes in -- reusing sessions across multiple conversations to amortize the initialization cost.

However, session pooling with Copilot SDK is tricky because sessions are stateful. If you naively return a session to the pool after a conversation ends, the next user who gets that session will inherit the previous conversation's context. This is context contamination, and it's a serious problem for both privacy and response quality.

Here's a safe approach to session pooling:

public class SafeSessionPool
{
    private readonly ConcurrentBag<CopilotSession> _availableSessions = new();
    private readonly CopilotClient _client;
    private readonly SessionConfig _config;
    
    public SafeSessionPool(CopilotClient client, SessionConfig config)
    {
        _client = client;
        _config = config;
    }
    
    public async Task<CopilotSession> AcquireAsync()
    {
        if (_availableSessions.TryTake(out var session))
        {
            // Return pooled session -- but only if we reset it first
            // Since we can't reset a session's internal state,
            // we must dispose and create a new one
            await session.DisposeAsync();
        }
        
        // Always create a fresh session to avoid context contamination
        return await _client.CreateSessionAsync(_config);
    }
    
    public void Release(CopilotSession session)
    {
        // Don't actually pool the session due to context contamination risk
        // This is more of a "managed creation" pattern than true pooling
        _ = session.DisposeAsync();
    }
}

As you can see, true session pooling with context preservation isn't viable for multi-user applications. What you can pool is the infrastructure -- connection pools, client instances, and so on -- but sessions themselves should be ephemeral and user-specific. The pattern above is more accurately described as "managed session creation" rather than pooling.

Where session pooling does make sense is in single-user scenarios with parallel processing. If one user is triggering multiple AI tasks simultaneously (like analyzing multiple code files), you might maintain a small pool of sessions for that user's workload, understanding that context leakage between their own tasks is acceptable or even desired.

Context Injection from External Sources

One of the most practical patterns for managing sessions GitHub Copilot SDK C# developers use is injecting context from external data sources directly into your prompts. This is a lightweight form of retrieval-augmented generation (RAG) that doesn't require vector databases or embedding models -- you just pull relevant data and include it in the user's message.

Here's how I typically implement context injection from a database:

public async Task<string> AskWithContextAsync(
    CopilotSession session, 
    string userQuestion,
    string userId)
{
    // Load relevant context from your data store
    var userPrefs = await _userRepo.GetPreferencesAsync(userId);
    var recentActivity = await _activityRepo.GetRecentAsync(userId, limit: 5);
    
    // Build context-enriched prompt
    var contextualPrompt = $"""
        User Context:
        - Name: {userPrefs.DisplayName}
        - Preferred language: {userPrefs.ProgrammingLanguage}
        - Recent activity: {string.Join(", ", recentActivity.Select(a => a.Description))}
        
        User Question: {userQuestion}
        """;
    
    var tcs = new TaskCompletionSource<string>();
    var sb = new System.Text.StringBuilder();
    
    session.On(evt =>
    {
        switch (evt)
        {
            case AssistantMessageEvent msg: sb.Append(msg.Data.Content); break;
            case SessionIdleEvent: tcs.TrySetResult(sb.ToString()); break;
            case SessionErrorEvent err: tcs.TrySetException(new Exception(err.Data.Message)); break;
        }
    });
    
    await session.SendAsync(new MessageOptions { Prompt = contextualPrompt });
    return await tcs.Task;
}

This pattern is remarkably effective. When a user asks "what should I work on next?" the model can see their recent activity and preferences, leading to much more relevant recommendations. When they ask "how do I implement feature X?" the model might see that they prefer C# and can tailor code examples accordingly.

The key is to be selective about what context you inject. Don't dump your entire database into the prompt -- choose the data that's most relevant to the current question. This keeps token usage reasonable and improves response quality by reducing noise. I often use simple keyword matching or category-based rules to decide what context to include for a given question.

Long-Running Agentic Sessions

Some AI applications need sessions that stay alive for extended periods while the agent performs complex tasks. Think of an AI that's monitoring a production system, analyzing logs over hours, and occasionally reporting findings. These long-running agentic sessions have different requirements than quick request-response interactions.

The main challenge with long-running sessions is keeping them alive and handling disconnections gracefully. The Copilot SDK maintains sessions through active connections, so if your application crashes or loses network connectivity, the session is lost. Here's a pattern for resilient long-running sessions:

public class ResilientAgentSession
{
    private readonly CopilotClient _client;
    private readonly SessionConfig _config;
    private CopilotSession? _session;
    private readonly Timer _heartbeatTimer;
    private DateTime _lastActivity;
    
    public ResilientAgentSession(CopilotClient client, SessionConfig config)
    {
        _client = client;
        _config = config;
        _heartbeatTimer = new Timer(HeartbeatCallback, null, 
            TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
    }
    
    public async Task InitializeAsync()
    {
        _session = await _client.CreateSessionAsync(_config);
        _lastActivity = DateTime.UtcNow;
    }
    
    private async void HeartbeatCallback(object? state)
    {
        if (DateTime.UtcNow - _lastActivity > TimeSpan.FromMinutes(30))
        {
            // Session has been idle too long, refresh it
            await RefreshSessionAsync();
        }
    }
    
    private async Task RefreshSessionAsync()
    {
        if (_session is not null)
        {
            await _session.DisposeAsync();
        }
        
        _session = await _client.CreateSessionAsync(_config);
        _lastActivity = DateTime.UtcNow;
    }
    
    public async Task<string> SendWithRetryAsync(string message)
    {
        try
        {
            _lastActivity = DateTime.UtcNow;
            return await SendInternalAsync(message);
        }
        catch (Exception)
        {
            // Connection lost, try to recover
            await RefreshSessionAsync();
            return await SendInternalAsync(message);
        }
    }
    
    private async Task<string> SendInternalAsync(string message)
    {
        if (_session is null)
            throw new InvalidOperationException("Session not initialized");
            
        var tcs = new TaskCompletionSource<string>();
        var sb = new System.Text.StringBuilder();
        
        _session.On(evt =>
        {
            switch (evt)
            {
                case AssistantMessageEvent msg: sb.Append(msg.Data.Content); break;
                case SessionIdleEvent: tcs.TrySetResult(sb.ToString()); break;
                case SessionErrorEvent err: tcs.TrySetException(new Exception(err.Data.Message)); break;
            }
        });
        
        await _session.SendAsync(new MessageOptions { Prompt = message });
        return await tcs.Task;
    }
}

This pattern implements heartbeat checks and automatic session recovery. If the session dies unexpectedly, the next message attempt will recreate it. The downside is that you lose conversation context on recovery, which is why the next pattern is important for truly resilient agents.

Session State Persistence

The GitHub Copilot SDK provides session persistence through the WorkspacePath configuration and ResumeSessionAsync() method. By default, the SDK uses InfiniteSessions which automatically manages context through background compaction, but you can also explicitly persist and resume sessions across application restarts.

While the SDK handles much of the persistence automatically when you configure a workspace path, you may want additional custom persistence for application-specific needs like saving conversation metadata or implementing custom recovery logic:

Here's the reality of what this looks like: you need to capture the conversation history yourself and serialize it:

public class PersistentConversationService
{
    private readonly CopilotClient _client;
    private readonly IConversationStore _store;
    private CopilotSession? _session;
    private readonly List<ConversationTurn> _history = new();
    private readonly string _conversationId;
    
    public PersistentConversationService(
        CopilotClient client, 
        IConversationStore store,
        string conversationId)
    {
        _client = client;
        _store = store;
        _conversationId = conversationId;
    }
    
    public async Task InitializeAsync()
    {
        // Try to load existing conversation
        var savedHistory = await _store.LoadHistoryAsync(_conversationId);
        if (savedHistory != null)
        {
            _history.AddRange(savedHistory);
        }
        
        _session = await _client.CreateSessionAsync(new SessionConfig
        {
            Model = "gpt-5",
            Streaming = true
        });
        
        // Replay history to rebuild context
        foreach (var turn in _history)
        {
            await ReplayTurnAsync(turn);
        }
    }
    
    private async Task ReplayTurnAsync(ConversationTurn turn)
    {
        // Send the historical message to rebuild context
        // Note: This costs tokens but restores the conversation state
        await _session!.SendAsync(new MessageOptions { Prompt = turn.UserMessage });
        // The assistant will respond, which rebuilds the context
    }
    
    public async Task<string> SendAsync(string userMessage)
    {
        if (_session is null)
            throw new InvalidOperationException("Call InitializeAsync first");
        
        var response = await SendInternalAsync(userMessage);
        
        // Save this turn
        var turn = new ConversationTurn
        {
            UserMessage = userMessage,
            AssistantResponse = response,
            Timestamp = DateTime.UtcNow
        };
        
        _history.Add(turn);
        await _store.SaveTurnAsync(_conversationId, turn);
        
        return response;
    }
    
    private async Task<string> SendInternalAsync(string message)
    {
        var tcs = new TaskCompletionSource<string>();
        var sb = new System.Text.StringBuilder();
        
        _session!.On(evt =>
        {
            switch (evt)
            {
                case AssistantMessageEvent msg: sb.Append(msg.Data.Content); break;
                case SessionIdleEvent: tcs.TrySetResult(sb.ToString()); break;
                case SessionErrorEvent err: tcs.TrySetException(new Exception(err.Data.Message)); break;
            }
        });
        
        await _session.SendAsync(new MessageOptions { Prompt = message });
        return await tcs.Task;
    }
}

public record ConversationTurn
{
    public required string UserMessage { get; init; }
    public required string AssistantResponse { get; init; }
    public DateTime Timestamp { get; init; }
}

public interface IConversationStore
{
    Task<List<ConversationTurn>?> LoadHistoryAsync(string conversationId);
    Task SaveTurnAsync(string conversationId, ConversationTurn turn);
}

This approach manually tracks every conversation turn and saves it to your chosen storage mechanism (database, blob storage, whatever fits your architecture). When you need to restore a conversation, you replay the history by sending each message through a fresh session. This is token-expensive since you're effectively re-running the entire conversation, but it's the only way to restore context given the SDK's in-memory design.

For very long conversations, you might implement history truncation or summarization strategies -- keeping only the last N turns or having the AI summarize older context before replaying.

FAQ

Does the Copilot SDK automatically manage context window limits?

Yes, by default the SDK uses InfiniteSessions which automatically manages context through background compaction when approaching token limits. You'll see SessionCompactionStartEvent and SessionCompactionCompleteEvent events when this happens. However, if you disable InfiniteSessions or want more control, you can implement your own strategy for managing sessions GitHub Copilot SDK C# applications, whether that's creating a new session when conversations get too long, implementing sliding window context, or using summarization to compress history. The automatic context management is one of the key features that makes the SDK production-ready.

Source: GitHub Copilot SDK .NET README

Can I share a session between multiple users?

Technically yes, but you shouldn't. Sessions maintain conversation context, and if multiple users share a session, they'll see each other's conversation history in the model's responses. This is a privacy issue and will also lead to confusing and incorrect responses. Always use one session per user conversation, never share sessions across users unless they're explicitly collaborating on the same conversation.

How do I know how many tokens my conversation has used?

The GitHub Copilot SDK doesn't expose token counting directly. You can estimate token usage by using the general rule that one token is roughly 4 characters in English text, or you can use third-party token counting libraries that match the tokenizer of your target model. For production applications, I recommend implementing approximate token tracking and creating new sessions when you estimate you're at 75-80% of the model's context window.

What happens to the session if my application crashes?

The session is lost. Sessions exist only in memory for the duration of the connection. If your process crashes, all active sessions and their conversation context disappear. This is why the persistent conversation pattern in the previous section is important for applications that need to survive restarts or recover from failures gracefully.

Conclusion

Managing sessions and context in the GitHub Copilot SDK for C# is fundamental to building production-ready AI applications. When you're managing sessions GitHub Copilot SDK C# applications, I've walked you through the core patterns -- session-per-conversation for clean lifecycle management, context injection for enriching responses with external data, session resilience for long-running agents, and manual persistence for continuity across restarts. Each pattern addresses different requirements, and in real applications you'll often combine several of them.

The key takeaway is that sessions are stateful resources that require careful management. Unlike simple API calls, sessions accumulate context over time, making them both more valuable and more complex to handle correctly. Whether you're building a quick chatbot or a sophisticated agentic system, understanding how context flows through your application will make the difference between an AI that feels forgetful and frustrating versus one that feels intelligent and aware. Proper session management and context handling are essential skills for managing sessions GitHub Copilot SDK C# developers working on production AI applications.

If you want to dive deeper into the fundamentals, check out my complete guide to the GitHub Copilot SDK for .NET and the getting started sub-hub. For the foundational concepts behind these patterns, my article on CopilotClient and CopilotSession core concepts will give you the building blocks you need. Now go build something intelligent -- and make sure it remembers what your users told it three messages ago.

Getting Started with GitHub Copilot SDK in C#: Installation, Setup, and First Conversation

Getting started with GitHub Copilot SDK in C#: master installation, CopilotClient setup, streaming responses, and build your first .NET AI app.

AgentSession and Multi-Turn Conversations in Microsoft Agent Framework

Master AgentSession and multi-turn conversations in Microsoft Agent Framework. Session lifecycle, concurrent sessions, and a complete stateful Q&A loop in C#.

CopilotClient and CopilotSession in C#: Core Concepts of the GitHub Copilot SDK

Master CopilotClient and CopilotSession -- the two core classes of the GitHub Copilot SDK in C#. Learn the client/session model, session configuration, lifecycle management, the event-driven API, and how they work together in .NET applications.

An error has occurred. This application may no longer respond until reloaded. Reload