| applyTo | description | name |
|---|---|---|
**.cs, **.csproj |
This file provides guidance on building C# applications using GitHub Copilot SDK. |
GitHub Copilot SDK C# Instructions |
- The SDK is in technical preview and may have breaking changes
- Requires .NET 10.0 or later
- Requires GitHub Copilot CLI installed and in PATH
- Uses async/await patterns throughout
- Implements IAsyncDisposable for resource cleanup
Always install via NuGet:
dotnet add package GitHub.Copilot.SDKawait using var client = new CopilotClient();
await client.StartAsync();When creating a CopilotClient, use CopilotClientOptions:
CliPath- Path to CLI executable (default: "copilot" from PATH)CliArgs- Extra arguments prepended before SDK-managed flagsCliUrl- URL of existing CLI server (e.g., "localhost:8080"). When provided, client won't spawn a processPort- Server port (default: 0 for random)UseStdio- Use stdio transport instead of TCP (default: true)LogLevel- Log level (default: "info")AutoStart- Auto-start server (default: true)AutoRestart- Auto-restart on crash (default: true)Cwd- Working directory for the CLI processEnvironment- Environment variables for the CLI processLogger- ILogger instance for SDK logging
For explicit control:
var client = new CopilotClient(new CopilotClientOptions { AutoStart = false });
await client.StartAsync();
// Use client...
await client.StopAsync();Use ForceStopAsync() when StopAsync() takes too long.
Use SessionConfig for configuration:
await using var session = await client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
Model = "gpt-5",
Streaming = true,
Tools = [...],
SystemMessage = new SystemMessageConfig { ... },
AvailableTools = ["tool1", "tool2"],
ExcludedTools = ["tool3"],
Provider = new ProviderConfig { ... }
});SessionId- Custom session IDModel- Model name ("gpt-5", "claude-sonnet-4.5", etc.)Tools- Custom tools exposed to the CLISystemMessage- System message customizationAvailableTools- Allowlist of tool namesExcludedTools- Blocklist of tool namesProvider- Custom API provider configuration (BYOK)Streaming- Enable streaming response chunks (default: false)
var session = await client.ResumeSessionAsync(sessionId, new ResumeSessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
// ...
});session.SessionId- Get session identifiersession.SendAsync(new MessageOptions { Prompt = "...", Attachments = [...] })- Send messagesession.AbortAsync()- Abort current processingsession.GetMessagesAsync()- Get all events/messagesawait session.DisposeAsync()- Clean up resources
ALWAYS use TaskCompletionSource for waiting on session events:
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 = "..." });
await done.Task;The On() method returns an IDisposable:
var subscription = session.On(evt => { /* handler */ });
// Later...
subscription.Dispose();Use pattern matching or switch expressions for event handling:
session.On(evt =>
{
switch (evt)
{
case UserMessageEvent userMsg:
// Handle user message
break;
case AssistantMessageEvent assistantMsg:
Console.WriteLine(assistantMsg.Data.Content);
break;
case ToolExecutionStartEvent toolStart:
// Tool execution started
break;
case ToolExecutionCompleteEvent toolComplete:
// Tool execution completed
break;
case SessionStartEvent start:
// Session started
break;
case SessionIdleEvent idle:
// Session is idle (processing complete)
break;
case SessionErrorEvent error:
Console.WriteLine($"Error: {error.Data.Message}");
break;
}
});Set Streaming = true in SessionConfig:
var session = await client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
Model = "gpt-5",
Streaming = true
});Handle both delta events (incremental) and final events:
var done = new TaskCompletionSource();
session.On(evt =>
{
switch (evt)
{
case AssistantMessageDeltaEvent delta:
// Incremental text chunk
Console.Write(delta.Data.DeltaContent);
break;
case AssistantReasoningDeltaEvent reasoningDelta:
// Incremental reasoning chunk (model-dependent)
Console.Write(reasoningDelta.Data.DeltaContent);
break;
case AssistantMessageEvent msg:
// Final complete message
Console.WriteLine("\n--- Final ---");
Console.WriteLine(msg.Data.Content);
break;
case AssistantReasoningEvent reasoning:
// Final reasoning content
Console.WriteLine("--- Reasoning ---");
Console.WriteLine(reasoning.Data.Content);
break;
case SessionIdleEvent:
done.SetResult();
break;
}
});
await session.SendAsync(new MessageOptions { Prompt = "Tell me a story" });
await done.Task;Note: Final events (AssistantMessageEvent, AssistantReasoningEvent) are ALWAYS sent regardless of streaming setting.
Use Microsoft.Extensions.AI.AIFunctionFactory.Create for type-safe tools:
using Microsoft.Extensions.AI;
using System.ComponentModel;
var session = await client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
Model = "gpt-5",
Tools = [
AIFunctionFactory.Create(
async ([Description("Issue ID")] string id) => {
var issue = await FetchIssueAsync(id);
return issue;
},
"lookup_issue",
"Fetch issue details from tracker"),
]
});- Return any JSON-serializable value (automatically wrapped)
- Or return
ToolResultAIContentwrappingToolResultObjectfor full control over metadata
When Copilot invokes a tool, the client automatically:
- Runs your handler function
- Serializes the return value
- Responds to the CLI
var session = await client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
Model = "gpt-5",
SystemMessage = new SystemMessageConfig
{
Mode = SystemMessageMode.Append,
Content = @"
<workflow_rules>
- Always check for security vulnerabilities
- Suggest performance improvements when applicable
</workflow_rules>
"
}
});var session = await client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
Model = "gpt-5",
SystemMessage = new SystemMessageConfig
{
Mode = SystemMessageMode.Replace,
Content = "You are a helpful assistant."
}
});Attach files to messages using UserMessageDataAttachmentsItem:
await session.SendAsync(new MessageOptions
{
Prompt = "Analyze this file",
Attachments = new List<UserMessageDataAttachmentsItem>
{
new UserMessageDataAttachmentsItem
{
Type = UserMessageDataAttachmentsItemType.File,
Path = "/path/to/file.cs",
DisplayName = "My File"
}
}
});Use the Mode property in MessageOptions:
"enqueue"- Queue message for processing"immediate"- Process message immediately
await session.SendAsync(new MessageOptions
{
Prompt = "...",
Mode = "enqueue"
});Sessions are independent and can run concurrently:
var session1 = await client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
Model = "gpt-5",
});
var session2 = await client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
Model = "claude-sonnet-4.5",
});
await session1.SendAsync(new MessageOptions { Prompt = "Hello from session 1" });
await session2.SendAsync(new MessageOptions { Prompt = "Hello from session 2" });Use custom API providers via ProviderConfig:
var session = await client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
Provider = new ProviderConfig
{
Type = "openai",
BaseUrl = "https://api.openai.com/v1",
ApiKey = "your-api-key"
}
});var sessions = await client.ListSessionsAsync();
foreach (var metadata in sessions)
{
Console.WriteLine($"Session: {metadata.SessionId}");
}await client.DeleteSessionAsync(sessionId);var state = client.State;try
{
var session = await client.CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll });
await session.SendAsync(new MessageOptions { Prompt = "Hello" });
}
catch (StreamJsonRpc.RemoteInvocationException ex)
{
Console.Error.WriteLine($"JSON-RPC Error: {ex.Message}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
}Monitor SessionErrorEvent for runtime errors:
session.On(evt =>
{
if (evt is SessionErrorEvent error)
{
Console.Error.WriteLine($"Session Error: {error.Data.Message}");
}
});Use PingAsync to verify server connectivity:
var response = await client.PingAsync("test message");ALWAYS use await using for automatic disposal:
await using var client = new CopilotClient();
await using var session = await client.CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll });
// Resources automatically cleaned upIf not using await using:
var client = new CopilotClient();
try
{
await client.StartAsync();
// Use client...
}
finally
{
await client.StopAsync();
}- Always use
await usingfor CopilotClient and CopilotSession - Use TaskCompletionSource to wait for SessionIdleEvent
- Handle SessionErrorEvent for robust error handling
- Use pattern matching (switch expressions) for event handling
- Enable streaming for better UX in interactive scenarios
- Use AIFunctionFactory for type-safe tool definitions
- Dispose event subscriptions when no longer needed
- Use SystemMessageMode.Append to preserve safety guardrails
- Provide descriptive tool names and descriptions for better model understanding
- Handle both delta and final events when streaming is enabled
await using var client = new CopilotClient();
await client.StartAsync();
await using var session = await client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
Model = "gpt-5"
});
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 = "What is 2+2?" });
await done.Task;await using var session = await client.CreateSessionAsync(new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll });
async Task SendAndWait(string prompt)
{
var done = new TaskCompletionSource();
var subscription = 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 = prompt });
await done.Task;
subscription.Dispose();
}
await SendAndWait("What is the capital of France?");
await SendAndWait("What is its population?");var session = await client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
Tools = [
AIFunctionFactory.Create(
([Description("User ID")] string userId) => {
return new {
Id = userId,
Name = "John Doe",
Email = "john@example.com",
Role = "Developer"
};
},
"get_user",
"Retrieve user information")
]
});