You've been using Copilot in your IDE for months. Completions, chat, maybe even agent mode for bigger refactors. But what happens when you want that same agentic capability inside your own application — a CLI tool for your team, an internal code review bot, or a customer support agent that can actually read your codebase? Until this week, you were on your own. Build your own orchestration loop, manage tool calls, handle context windows, and pray your prompt engineering holds up.

The GitHub Copilot SDK, which entered public preview on 2 April 2026, changes this. It exposes the same production-tested agent runtime that powers Copilot CLI as a proper SDK you can embed in any .NET application. No need to reinvent multi-turn sessions, tool invocation plumbing, or streaming infrastructure. Install a NuGet package and you're building agents.

Let's look at what it actually offers and how to use it from C#.

What the SDK gives you

The Copilot SDK is not a thin wrapper around an LLM API. It's the full Copilot agent runtime — the same engine that handles planning loops, tool orchestration, file operations, and multi-turn conversations in Copilot CLI. When you use the SDK, your application gets:

The SDK communicates with a local Copilot CLI process over JSON-RPC. It manages the CLI lifecycle automatically — starting, stopping, and reconnecting as needed. You can also point it at an already-running CLI server if you prefer to manage the process yourself.

Getting started

Install the package:

terminal
dotnet add package GitHub.Copilot.SDK

The SDK requires the GitHub Copilot CLI to be installed and available. Authentication works through your existing GitHub credentials, a GitHub App token, or standard environment variables (GH_TOKEN, GITHUB_TOKEN).

Here's the simplest possible agent — send a prompt, print the response:

Program.cs
using GitHub.Copilot.SDK;

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

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

var done = new TaskCompletionSource();

session.On(evt =>
{
    if (evt is AssistantMessageEvent msg)
        Console.WriteLine(msg.Data.Content);
    else if (evt is SessionIdleEvent)
        done.SetResult();
});

await session.SendAsync(new MessageOptions { Prompt = "Explain the builder pattern in C#" });
await done.Task;

A few things to note about this pattern. The CopilotClient is expensive to create — you should have one per application lifetime. Sessions, on the other hand, are cheap. Create one per conversation or task. The event-driven model means you subscribe to session events and react as they arrive, rather than awaiting a single response.

// IMPORTANT

OnPermissionRequest is mandatory. When the agent decides to use a tool (read a file, run a command), this handler determines whether to allow it. PermissionHandler.ApproveAll is fine for development, but you'll want something more considered in production.

Defining custom tools

The real power comes when you expose your own functions to the agent. The SDK uses AIFunctionFactory to convert regular C# methods into tools the agent can invoke. It reads method signatures and [Description] attributes to generate the JSON schema the model needs.

Tools/InventoryTools.cs
using System.ComponentModel;
using GitHub.Copilot.SDK;

public class InventoryTools
{
    private readonly IInventoryService _inventory;

    public InventoryTools(IInventoryService inventory)
    {
        _inventory = inventory;
    }

    [Description("Check stock levels for a product by its SKU code")]
    public async Task<string> CheckStock(
        [Description("The product SKU, e.g. WH-1000XM5")] string sku)
    {
        var level = await _inventory.GetStockLevelAsync(sku);
        return level is null
            ? $"No product found with SKU '{sku}'"
            : $"SKU {sku}: {level.Quantity} units in stock, warehouse {level.Location}";
    }

    [Description("Place a restocking order for a product")]
    public async Task<string> PlaceRestockOrder(
        [Description("The product SKU")] string sku,
        [Description("Number of units to order")] int quantity)
    {
        var order = await _inventory.CreateRestockOrderAsync(sku, quantity);
        return $"Restock order {order.Id} created: {quantity} units of {sku}";
    }
}

Register these tools when creating the session:

Program.cs
var inventoryTools = new InventoryTools(inventoryService);

var session = await client.CreateSessionAsync(new SessionConfig
{
    Model = "gpt-5",
    Tools = new List<AIFunction>
    {
        AIFunctionFactory.Create(inventoryTools.CheckStock, name: "check_stock"),
        AIFunctionFactory.Create(inventoryTools.PlaceRestockOrder, name: "place_restock_order"),
    },
    OnPermissionRequest = async req =>
    {
        // Allow read operations automatically, prompt for writes
        if (req.ToolName == "check_stock")
            return PermissionResponse.Allow;

        logger.LogInformation("Agent wants to call {Tool} with {Args}", req.ToolName, req.Arguments);
        return PermissionResponse.Allow; // Or Deny, based on your logic
    },
});

The [Description] attributes are not optional fluff — they're the primary mechanism the model uses to decide which tool to call and what arguments to pass. Be specific. A description like "Does stock things" will produce poor results.

// TIP

Async methods returning Task<string> or Task<T> are fully supported. The SDK awaits them automatically during invocation. Use this for database lookups, HTTP calls, or any I/O-bound operation your tools need.

Streaming responses

For interactive applications, you don't want to wait for the full response before showing anything. Enable streaming in the session configuration:

AgentHost.cs
var session = await client.CreateSessionAsync(new SessionConfig
{
    Model = "gpt-5",
    Streaming = true,
    OnPermissionRequest = PermissionHandler.ApproveAll,
});

session.On(evt =>
{
    switch (evt)
    {
        case AssistantMessageDeltaEvent delta:
            Console.Write(delta.Data.DeltaContent);
            break;
        case AssistantMessageEvent complete:
            Console.WriteLine(); // Final newline after all deltas
            break;
        case ToolExecutionStartEvent toolStart:
            Console.WriteLine($"[Calling {toolStart.Data.ToolName}...]");
            break;
        case ToolExecutionCompleteEvent toolDone:
            Console.WriteLine($"[{toolDone.Data.ToolName} complete]");
            break;
        case SessionIdleEvent:
            // Agent finished processing
            break;
    }
});

The event types follow a clear pattern. AssistantMessageDeltaEvent fires for each token chunk during streaming, while AssistantMessageEvent carries the complete message once generation finishes. Tool execution events let you show progress indicators when the agent is working on something.

Bringing your own model provider

The BYOK (Bring Your Own Key) option is particularly interesting for teams that can't use GitHub Copilot subscriptions due to procurement constraints or data residency requirements. You configure a custom provider in the session:

Program.cs
var session = await client.CreateSessionAsync(new SessionConfig
{
    Provider = new ProviderConfig
    {
        BaseUrl = "https://your-azure-openai.openai.azure.com/",
        ApiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY"),
        ModelId = "gpt-4o",
    },
    OnPermissionRequest = PermissionHandler.ApproveAll,
});

This works with OpenAI, Azure AI Foundry, and Anthropic endpoints. The SDK handles the protocol differences — you get the same CopilotSession API regardless of the backing model.

// NOTE

BYOK uses key-based authentication only. Microsoft Entra ID, managed identities, and third-party identity providers are not supported in the current preview.

Session lifecycle and the event model

Understanding the session lifecycle matters when building anything beyond a one-shot script. Here's the full event taxonomy:

Event When it fires
SessionStartEvent Session begins processing a message
AssistantMessageDeltaEvent Each streaming token (streaming mode only)
AssistantReasoningEvent Model's chain-of-thought reasoning
AssistantMessageEvent Complete assistant response
ToolExecutionStartEvent Agent begins a tool call
ToolExecutionCompleteEvent Tool call finishes
SessionIdleEvent Agent has finished all work
SessionErrorEvent Something went wrong

Pattern matching on the event base type keeps handler code clean:

EventLogger.cs
session.On(evt =>
{
    switch (evt)
    {
        case AssistantMessageEvent { Data.Content: var content }:
            logger.LogInformation("Assistant: {Content}", content);
            break;
        case ToolExecutionCompleteEvent { Data: var data }:
            logger.LogDebug("Tool {Name} returned: {Result}", data.ToolName, data.Result);
            break;
        case SessionErrorEvent { Data: var error }:
            logger.LogError("Session error: {Error}", error.Message);
            break;
    }
});

Client-level events track session lifecycle itself — creation, deletion, foregrounding, and backgrounding. Subscribe via client.On() to manage a pool of sessions across your application.

Infinite sessions

For long-running agents that accumulate context beyond the model's window, the SDK offers automatic context compaction:

Example.cs
var session = await client.CreateSessionAsync(new SessionConfig
{
    Model = "gpt-5",
    InfiniteSessions = new InfiniteSessionConfig { Enabled = true },
    OnPermissionRequest = PermissionHandler.ApproveAll,
});

When enabled, the session writes checkpoints to a workspace directory containing a plan.md, checkpoint data, and relevant files. As context grows, older turns are compacted while preserving the essential information the agent needs. You can access this workspace via session.WorkspacePath.

This is the feature that separates the SDK from raw API calls. Building reliable context compaction from scratch is a months-long project. Getting it for free is significant.

Where this fits alongside Microsoft.Extensions.AI

If you've been following the .NET AI ecosystem, you might wonder how this relates to Microsoft.Extensions.AI — the abstraction layer that provides IChatClient and IEmbeddingGenerator interfaces.

They solve different problems. Microsoft.Extensions.AI gives you a unified abstraction for calling language models. You implement IChatClient once and swap between OpenAI, Azure, Ollama, or any other provider. It's an abstraction over inference.

The Copilot SDK gives you a complete agent runtime. Planning loops, tool orchestration, multi-turn state management, context compaction — the whole execution layer that sits above raw model calls. If Microsoft.Extensions.AI is the HTTP client, the Copilot SDK is the web framework.

In practice, you'd use Microsoft.Extensions.AI when you need direct model access with clean abstractions — embeddings, classification, straightforward chat. You'd use the Copilot SDK when you need an agent that autonomously plans and executes multi-step tasks using tools.

Common pitfalls

Forgetting the permission handler. OnPermissionRequest is required, not optional. The SDK won't create a session without it. Don't blindly use PermissionHandler.ApproveAll in production — a rogue tool call against your database is not the kind of surprise you want.

Creating multiple CopilotClient instances. Each client spawns a CLI process. One client per application is the correct pattern. Create multiple sessions off a single client instead.

Ignoring error events. SessionErrorEvent is easy to overlook when you're focused on happy-path events. Always handle it. The agent might fail mid-tool-execution, and without an error handler, your application will silently stall waiting for a SessionIdleEvent that never arrives.

Vague tool descriptions. The model decides which tool to call based on the [Description] attribute text. Generic descriptions like "handles data" or "utility function" produce unreliable tool selection. Be as specific as the parameter name itself — what does it do, what format does the input expect, what does it return.

Blocking the event loop. Event handlers run synchronously by default. If your handler does slow I/O (logging to a remote service, database writes), you'll delay subsequent events. Offload heavy work to a background queue.

Summary