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

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

If you want to truly master the GitHub Copilot SDK for .NET, you need to understand CopilotClient and CopilotSession in C#. These two classes form the foundation of everything you'll do with the SDK. CopilotClient manages your connection to the GitHub CLI process, while CopilotSession represents an individual conversation context with the AI. Understanding how they work together, their lifecycle patterns, and their configuration options will unlock the full potential of building AI-powered applications in .NET. In this deep dive, I'll walk you through every aspect of these core components so you can use them confidently in your projects.

CopilotClient: The Connection Manager

The CopilotClient class is your entry point to the GitHub Copilot SDK. It manages the connection to the underlying GitHub CLI process that communicates with the Copilot service. Think of it as the infrastructure layer -- it handles process lifecycle, network communication, and provides the factory methods for creating sessions.

When you create a CopilotClient instance, you can optionally provide CopilotClientOptions to customize its behavior. These options allow you to specify the path to the GitHub CLI executable if it's not in your system PATH, or configure a different port if the default one conflicts with other services on your machine.

// CopilotClient options
using GitHub.Copilot.SDK;

var options = new CopilotClientOptions
{
    // Optional: specify explicit path to gh CLI if not in PATH
    // CliPath = @"C:Program FilesGitHub CLIgh.exe",
    
    // Optional: specify a different port if default conflicts
    // Port = 8765
};

await using var client = new CopilotClient(options);
await client.StartAsync();
Console.WriteLine("Client started -- CLI process is running");

The StartAsync() method is crucial. Until you call it, the client hasn't actually started the GitHub CLI process. This means no connection to Copilot services exists yet. Once started, the client maintains that connection and manages the underlying process lifecycle. If the process crashes or terminates unexpectedly, the client will detect it and surface appropriate errors.

Lifecycle considerations matter here. The CopilotClient implements IAsyncDisposable, so you should always use the await using pattern to ensure proper cleanup. When disposed, the client terminates the CLI process and releases all associated resources.

CopilotClient as a Singleton

In most applications, you should treat CopilotClient as a singleton. Starting multiple clients means spawning multiple GitHub CLI processes, each consuming memory and system resources. More importantly, there's rarely a good reason to have multiple independent connections to the Copilot service in a single application.

The typical pattern in a .NET application using dependency injection is to register the client as a singleton service. However, there's a timing challenge: the client needs to call StartAsync() before it can be used, but DI containers don't have a built-in way to handle async initialization during service registration.

The solution is to use an IHostedService implementation that starts the client during application startup:

using GitHub.Copilot.SDK;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

public class CopilotClientHostedService : IHostedService
{
    private readonly CopilotClient _client;

    public CopilotClientHostedService(CopilotClient client)
    {
        _client = client;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        await _client.StartAsync();
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        // Client disposal is handled by DI container
        return Task.CompletedTask;
    }
}

// In Program.cs or Startup.cs
services.AddSingleton(sp => new CopilotClient(new CopilotClientOptions()));
services.AddHostedService<CopilotClientHostedService>();

This pattern ensures your CopilotClient is started when your application starts, and properly disposed when the application shuts down. Any service that depends on CopilotClient can safely inject it and create sessions as needed.

CopilotSession: Your Conversation Context

While CopilotClient manages the connection infrastructure, CopilotSession represents an actual conversation with the AI. Each session maintains its own conversation history, context, and state. This separation is powerful because it allows you to have multiple independent conversations happening simultaneously through the same client connection.

A session is stateful. When you send a message through a session, the AI remembers that message and all previous messages in that session's history. This enables natural, context-aware conversations where the AI can reference earlier parts of the discussion. It's exactly like having multiple chat windows open with different topics -- each maintains its own conversation flow.

Creating a session is straightforward through the client's CreateSessionAsync() method. Each session is independent and isolated from others. You can create dozens of concurrent sessions if your application needs to handle multiple users or parallel AI tasks. The client manages the underlying communication efficiently, routing messages to and from the correct session.

The key insight here is separation of concerns: the client handles the "how do I talk to Copilot at all" problem, while sessions handle the "what am I talking about" problem. You set up the client once, then create and dispose sessions as needed for each conversation or context in your application.

SessionConfig: Configuring Your Session

When you create a session, you provide a SessionConfig object that defines how that specific session behaves. This configuration gives you fine-grained control over model selection, streaming behavior, and tool availability.

The Model property lets you choose which AI model powers the session. As of early 2026, the SDK supports several models including gpt-5 (see the official SDK documentation for the full list of available models). claude-sonnet-4.5 is also confirmed as a supported model. Each model has different performance characteristics and cost implications, so choosing the right one depends on your use case.

The Streaming property is critical for user experience. When set to true, the AI streams its response token by token as it generates them, firing AssistantMessageDeltaEvent events for each chunk. This creates the familiar typewriter effect you see in ChatGPT and other modern AI interfaces. When false, the session waits for the complete response and fires a single AssistantMessageEvent with the full content.

The Tools property allows you to specify which tools (functions) the AI can call during the session. However, many applications prefer to register tools directly with the kernel instead of specifying them per-session.

Here's a complete SessionConfig example showing the most common configuration:

// SessionConfig exploration
await using var session = await client.CreateSessionAsync(new SessionConfig
{
    Model = "gpt-5",       // Strong reasoning capability -- verify current model options in SDK docs
    Streaming = true,         // Enable delta events for streaming output
    // Tools array can be set here or added to the kernel separately
});

Choosing the right configuration depends on your requirements. If you need the fastest possible response for simple queries, use gpt-5 with streaming disabled. For complex reasoning tasks where you want to show progress to users, use gpt-5 with streaming enabled. The flexibility is there to match your specific scenario.

The Event-Driven API: session.On(...)

The GitHub Copilot SDK uses an event-driven architecture for handling AI responses. Instead of blocking and waiting for a complete response, you register event handlers using the session.On(...) method. This design makes sense because the SDK is streaming-first -- responses arrive incrementally, and your application needs to react to each piece as it arrives.

The event handler receives instances of different event types, and you typically use a switch statement to handle each type appropriately. This pattern gives you complete control over how your application responds to different phases of the AI's interaction.

The SDK defines several event types that cover the entire lifecycle of an AI response:

// Handling all event types
session.On(evt =>
{
    switch (evt)
    {
        case AssistantMessageEvent msg:
            // Fires once with the complete message (when Streaming = false)
            Console.WriteLine($"[Complete] {msg.Data.Content}");
            break;
            
        case AssistantMessageDeltaEvent delta:
            // Fires for each token chunk (when Streaming = true)
            Console.Write(delta.Data.DeltaContent);
            break;
            
        case ToolExecutionStartEvent toolStart:
            // Fires when the AI calls one of your registered tools
            Console.WriteLine($"[Tool] {toolStart.Data.ToolName}({toolStart.Data.Arguments})");
            break;
            
        case SessionIdleEvent:
            // Fires when the AI has finished responding -- turn is complete
            Console.WriteLine("
[Done]");
            break;
            
        case SessionErrorEvent err:
            // Fires on any error -- always handle this
            Console.WriteLine($"[Error] {err.Data.Code}: {err.Data.Message}");
            break;
    }
});

This event-driven approach integrates naturally with modern async/await patterns in C#. You can use TaskCompletionSource to bridge between the event handlers and async methods, enabling clean integration with the rest of your async codebase.

AssistantMessageEvent vs AssistantMessageDeltaEvent

Understanding the difference between AssistantMessageEvent and AssistantMessageDeltaEvent is critical for correctly handling AI responses. These two events represent fundamentally different response patterns based on your SessionConfig.Streaming setting.

When Streaming is false, the session waits for the AI to generate the complete response internally before sending anything to your application. Once the full response is ready, you receive a single AssistantMessageEvent containing the entire message in the Content property. This is simpler to handle but provides no progress feedback to users during generation.

When Streaming is true, the session immediately starts forwarding response tokens as the AI generates them. You receive multiple AssistantMessageDeltaEvent instances, each containing a small chunk of text in the DeltaContent property. You're responsible for accumulating these chunks to build the complete message. After all deltas are sent, you'll receive a SessionIdleEvent indicating the AI has finished.

Here's the key pattern: if you need the complete message for processing, accumulate the deltas. If you're displaying output to a user, append each delta directly to your UI for the streaming typewriter effect. Many applications handle both events and use a flag or the session config to determine which code path to execute.

The choice between streaming and non-streaming depends on your UX requirements. Streaming creates a more responsive, engaging experience but requires slightly more complex event handling. Non-streaming is simpler but users see nothing until the entire response completes, which can feel slow for longer responses.

Session Scoping: One Session Per What?

A common question when working with CopilotClient and CopilotSession in C# is: how should I scope my sessions? Should I create one per user, one per conversation, one per request? The answer depends on how much context you want the AI to maintain and how you manage state in your application.

For web applications serving multiple users, the typical pattern is one session per user per conversation. If your app has a chat interface where users can have multiple independent chat threads, each thread gets its own session. This ensures each conversation maintains its own context without bleeding information between unrelated discussions.

For request-response scenarios like API endpoints where each request is independent, you might create a session per request and dispose it immediately after responding. This prevents context accumulation and keeps your memory footprint constant. However, you lose the ability to do multi-turn conversations where the AI references previous exchanges.

For batch processing or background tasks, you might reuse a single session for an entire operation if the tasks are logically related. For example, analyzing multiple code files might benefit from session continuity so the AI can reference patterns it noticed in earlier files.

The tradeoff is always between context accumulation and isolation. Longer-lived sessions build up conversation history, which improves the AI's ability to provide contextual answers but also increases memory usage and potentially costs. Shorter-lived sessions keep state minimal but require you to provide more context in each individual message.

My recommendation: start with the narrowest reasonable scope for your use case. If you need multi-turn conversations, go per-conversation. If each interaction is independent, go per-request. You can always widen the scope later if you find you need more context continuity.

IAsyncDisposable: Clean Disposal

Both CopilotClient and CopilotSession implement IAsyncDisposable, which means proper disposal is critical for clean resource management. Failing to dispose these objects can leave processes running, connections open, and memory allocated long after you're done with them.

The await using pattern is your best friend here. It ensures that disposal happens automatically when the object goes out of scope, even if exceptions occur:

await using var client = new CopilotClient();
await client.StartAsync();

await using var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" });

// Use session...

// Both session and client are automatically disposed here

What happens if you forget to dispose? For sessions, you leak resources on both the client side and potentially on the GitHub Copilot service side. The session's conversation history stays in memory, and the connection resources aren't released. For clients, you leave the GitHub CLI process running, consuming system resources indefinitely.

Disposal order matters when you have nested disposable objects. Always dispose the child objects before the parent. In practice, with await using, this happens automatically because the scope rules ensure inner objects are disposed before outer ones. Sessions should be disposed before the client they were created from.

If you're registering CopilotClient as a singleton in dependency injection, the DI container handles disposal automatically when the application shuts down. You don't need to manually dispose it. However, sessions created from that client are your responsibility -- dispose them when the conversation ends or the request completes.

Concurrent Sessions

One of the powerful features of the CopilotClient and CopilotSession architecture is support for concurrent sessions. You can create multiple sessions from a single client and use them simultaneously without conflicts. The client handles the routing and ensures messages reach the correct session.

This capability enables several interesting scenarios. You might run parallel queries with different models to compare responses, implement multi-agent systems where different AI personas handle different aspects of a problem, or serve multiple users concurrently in a web application.

Here's an example showing concurrent sessions querying two different models:

// Multiple concurrent sessions
await using var client = new CopilotClient();
await client.StartAsync();

// Create two independent sessions for parallel queries
var session1Task = client.CreateSessionAsync(new SessionConfig { Model = "gpt-5" });
var session2Task = client.CreateSessionAsync(new SessionConfig { Model = "gpt-4.1" });

await using var session1 = await session1Task;
await using var session2 = await session2Task;

// Run both queries concurrently
var query1 = GetResponseAsync(session1, "Explain async/await in one paragraph");
var query2 = GetResponseAsync(session2, "Explain dependency injection in one paragraph");

var results = await Task.WhenAll(query1, query2);
Console.WriteLine($"Fast query: {results[1]}");
Console.WriteLine($"Smart query: {results[0]}");

static async Task<string> GetResponseAsync(CopilotSession session, string prompt)
{
    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 = prompt });
    return await tcs.Task;
}

From a thread safety perspective, individual session objects are not thread-safe. Don't call methods on the same session instance from multiple threads concurrently. However, different session instances can be used from different threads without issue because the client handles synchronization of the underlying communication channel.

The most common use case for concurrent sessions in production applications is multi-tenant web servers where each HTTP request might create its own session. The shared singleton client efficiently manages all these concurrent conversations without requiring you to implement any locking or synchronization logic.

FAQ

These are some of the most common questions I encounter when developers are learning to work with CopilotClient and CopilotSession in their C# applications. Understanding these patterns will save you debugging time and help you make better architectural decisions from the start.

Do I need a separate CopilotClient for each user in my web application?

No, you should use a single shared CopilotClient singleton for the entire application. Create separate sessions per user or per conversation instead. The client is designed to efficiently handle many concurrent sessions.

Can I reuse a CopilotSession across multiple HTTP requests?

Yes, but you need to manage the session's lifetime appropriately. Store the session reference in user session state or a cache, and dispose it when the user's session ends or times out. Be aware that long-lived sessions accumulate conversation history which affects memory usage.

What happens if I don't call StartAsync() on CopilotClient?

Any attempt to create a session will fail because the underlying CLI process hasn't been started. Always call StartAsync() immediately after creating the client, ideally during application startup.

Can I change the model of an existing session?

No, the model is set when you create the session and cannot be changed afterward. If you need a different model, dispose the current session and create a new one with the desired configuration.

How do I know when an AI response is complete?

Watch for the SessionIdleEvent. This event fires when the AI has finished generating its response and the session is ready for the next user message. It's your signal that the conversation turn is complete.

Conclusion

Mastering CopilotClient and CopilotSession in C# is essential for building robust applications with the GitHub Copilot SDK. The client provides your connection infrastructure and should be treated as a singleton in most applications, while sessions represent individual conversations with independent context and history. Understanding the lifecycle patterns, configuration options, and event-driven API unlocks the full power of the SDK.

The key patterns to remember: use await using for proper disposal, configure sessions appropriately for your use case with SessionConfig, handle all event types in your session.On(...) handlers, and scope sessions based on your context requirements. With these concepts solid, you're ready to build sophisticated AI-powered features in your .NET applications.

If you're just getting started with the SDK, check out the Getting Started guide and the installation instructions to set up your environment. From there, understanding these core classes will give you the foundation to explore more advanced features like tool calling, kernel configuration, and building complete AI-powered workflows.

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.

GitHub Copilot SDK for .NET: Complete Developer Guide

Learn the GitHub Copilot SDK for .NET in this complete developer guide. Build custom AI agents with CopilotClient, CopilotSession, streaming, tools, and multi-model support in C#.

GitHub Copilot SDK Installation and Project Setup in C#: Step-by-Step Guide

Set up the GitHub Copilot SDK in C# with this step-by-step guide covering NuGet package install, GitHub Copilot CLI authentication, and project configuration.

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