From CLI Calls to SDK Integration: Best Practices for GitHub Copilot in .NET Projects
From CLI Calls to SDK Integration: Best Practices for GitHub Copilot in .NET Projects
Section titled “From CLI Calls to SDK Integration: Best Practices for GitHub Copilot in .NET Projects”The upgrade path from command-line invocation to official SDK integration has been quite a journey. Today, I want to share the pitfalls we ran into and what we learned while building HagiCode.
Background
Section titled “Background”After the GitHub Copilot SDK was officially released in 2025, we started integrating it into our AI capability layer. Before that, the project mainly used GitHub Copilot by directly invoking the Copilot CLI command-line tool, but that approach had several obvious issues:
- Complex process management: We had to manually manage the CLI process lifecycle, startup timeouts, and process cleanup. Processes can crash without warning.
- Incomplete event handling: Raw CLI invocation makes it hard to capture fine-grained events from model reasoning and tool execution. It is like seeing only the result without the thinking process.
- Difficult session management: There was no effective mechanism for session reuse and recovery, so every interaction had to start over.
- Compatibility problems: CLI arguments changed frequently, which meant we constantly had to maintain compatibility logic for those parameters.
These issues became increasingly apparent in day-to-day development, especially when we needed to track model reasoning (thinking) and tool execution status in real time. At that point, it became clear that we needed a lower-level and more complete integration approach.
About HagiCode
Section titled “About HagiCode”The approach shared in this article comes from our practical experience in the HagiCode project. HagiCode is an open-source AI coding assistant project, and during development we needed deep integration with GitHub Copilot capabilities, from basic code completion to complex multi-turn conversations and tool calling. Those real-world requirements pushed us to move from CLI invocation to the official SDK.
If this implementation sounds useful to you, there is a good chance our engineering experience can help. In that case, the HagiCode project itself may also be worth checking out. You might even find more project information and links at the end of this article.
Architecture Design
Section titled “Architecture Design”The project uses a layered architecture to address the limitations of CLI invocation:
┌─────────────────────────────────────────────────────────┐│ hagicode-core (Orleans Grains + AI Provider Layer) ││ - CopilotAIProvider: Converts AIRequest to CopilotOptions ││ - GitHubCopilotGrain: Orleans distributed execution interface │└─────────────────────────────────────────────────────────┘ ↓┌─────────────────────────────────────────────────────────┐│ HagiCode.Libs (Shared Provider Layer) ││ - CopilotProvider: CLI Provider interface implementation ││ - ICopilotSdkGateway: SDK invocation abstraction ││ - GitHubCopilotSdkGateway: SDK session management and event dispatch │└─────────────────────────────────────────────────────────┘ ↓┌─────────────────────────────────────────────────────────┐│ GitHub Copilot SDK (Official .NET SDK) ││ - CopilotClient: SDK client ││ - CopilotSession: Session management ││ - SessionEvent: Event stream │└─────────────────────────────────────────────────────────┘This layered design brings several practical technical advantages:
- Separation of concerns: Core business logic is decoupled from SDK implementation details.
- Testability: The
ICopilotSdkGatewayinterface makes unit testing straightforward. - Reusability:
HagiCode.Libscan be referenced by multiple projects. - Maintainability: SDK upgrades only require changes in the Gateway layer, while upper layers remain untouched.
Core Implementation
Section titled “Core Implementation”Authentication Flow
Section titled “Authentication Flow”Authentication is the first and most important step of SDK integration. If authentication fails, nothing else matters. We designed a flexible authentication configuration that supports multiple authentication sources:
// CopilotProvider.cs - Authentication source configurationpublic class CopilotOptions{ public bool UseLoggedInUser { get; set; } = true; public string? GitHubToken { get; set; } public string? CliUrl { get; set; }}
// Convert to SDK requestreturn new CopilotSdkRequest( GitHubToken: options.AuthSource == CopilotAuthSource.GitHubToken ? options.GitHubToken : null, UseLoggedInUser: options.AuthSource != CopilotAuthSource.GitHubToken);The benefits of this design are fairly clear:
- It supports logged-in user mode without requiring a token, which fits desktop scenarios well.
- It supports GitHub Token mode, which is suitable for server-side deployments and centralized management.
- It supports overriding the Copilot CLI URL, which helps with enterprise proxy configuration.
In practice, this flexible authentication model greatly simplified configuration across different deployment scenarios. The desktop client can use each user’s own Copilot login state, while the server side can manage authentication centrally through tokens.
Event Stream Handling
Section titled “Event Stream Handling”One of the most powerful capabilities of the SDK is complete event stream capture. We implemented an event dispatch system that can handle all kinds of SDK events in real time:
// GitHubCopilotSdkGateway.cs - Core logic for event dispatchinternal static SessionEventDispatchResult DispatchSessionEvent( SessionEvent evt, bool sawDelta){ switch (evt) { case AssistantReasoningEvent reasoningEvent: // Capture the model reasoning process events.Add(new CopilotSdkStreamEvent( CopilotSdkStreamEventType.ReasoningDelta, Content: reasoningEvent.Data.Content)); break;
case ToolExecutionStartEvent toolStartEvent: // Capture the start of a tool invocation events.Add(new CopilotSdkStreamEvent( CopilotSdkStreamEventType.ToolExecutionStart, ToolName: toolStartEvent.Data.ToolName, ToolCallId: toolStartEvent.Data.ToolCallId)); break;
case ToolExecutionCompleteEvent toolCompleteEvent: // Capture tool completion and its result events.Add(new CopilotSdkStreamEvent( CopilotSdkStreamEventType.ToolExecutionEnd, Content: ExtractToolExecutionContent(toolCompleteEvent))); break;
default: // Preserve unhandled events as RawEvent events.Add(new CopilotSdkStreamEvent( CopilotSdkStreamEventType.RawEvent, RawEventType: evt.GetType().Name)); break; }}The value of this implementation is significant:
- Complete capture of the model reasoning process (
thinking): users can see how the AI is reasoning, not just the final answer. - Real-time tracking of tool execution status: we know which tools are running, when they finish, and what they return.
- Zero event loss: by falling back to
RawEvent, we ensure every event is recorded.
In HagiCode, these fine-grained events help users understand how the AI works internally, especially when debugging complex tasks.
CLI Compatibility Handling
Section titled “CLI Compatibility Handling”After migrating from CLI invocation to the SDK, we found that some existing CLI parameters no longer applied in the SDK. To preserve backward compatibility, we implemented a parameter filtering system:
// CopilotCliCompatibility.cs - Argument filteringprivate static readonly Dictionary<string, string> RejectedFlags = new(){ ["--headless"] = "Unsupported startup argument", ["--model"] = "Passed through an SDK-native field", ["--prompt"] = "Passed through an SDK-native field", ["--interactive"] = "Interaction is managed by the provider",};
public static CopilotCliArgumentBuildResult BuildCliArgs(CopilotOptions options){ // Filter out unsupported arguments and keep compatible ones // Generate diagnostic information}This gives us several benefits:
- It automatically filters incompatible CLI arguments to prevent runtime errors.
- It generates clear diagnostic messages to help developers locate problems quickly.
- It keeps the SDK stable and insulated from changes in CLI parameters.
During the upgrade, this compatibility mechanism helped us transition smoothly. Existing configuration files could still be used and only needed gradual adjustments based on the diagnostic output.
Runtime Pooling
Section titled “Runtime Pooling”Creating Copilot SDK sessions is relatively expensive, and creating and destroying sessions too frequently hurts performance. To solve that, we implemented a session pool management system:
// CopilotProvider.cs - Session pool managementawait using var lease = await _poolCoordinator.AcquireCopilotRuntimeAsync( request, async ct => await _gateway.CreateRuntimeAsync(sdkRequest, ct), cancellationToken);
if (lease.IsWarmLease){ // Reuse an existing session yield return CreateSessionReusedMessage();}
await foreach (var eventData in lease.Entry.Resource.SendPromptAsync(...)){ yield return MapEvent(eventData);}The benefits of session pooling include:
- Session reuse: requests with the same
sessionIdcan reuse existing sessions and reduce startup overhead. - Session recovery support: after a network interruption, previous session state can be restored.
- Automatic pooling management: expired sessions are cleaned up automatically to avoid resource leaks.
In HagiCode, session pooling noticeably improved responsiveness, especially for continuous conversations.
Orleans Integration
Section titled “Orleans Integration”HagiCode uses Orleans as its distributed framework, and we integrated the Copilot SDK into Orleans Grains:
// GitHubCopilotGrain.cs - Distributed executionpublic async IAsyncEnumerable<GitHubCopilotResponse> ExecuteCommandStreamAsync( string command, CancellationToken token = default){ var provider = await aiProviderFactory.GetProviderAsync(AIProviderType.GitHubCopilot);
await foreach (var chunk in provider.SendMessageAsync(request, null, token)) { // Map to the unified response format yield return BuildChunkResponse(chunk, startedAt); }}The advantages of Orleans integration are substantial:
- Unified AI Provider abstraction: it becomes easy to switch between different AI providers.
- Multi-tenant isolation: Copilot sessions for different users remain isolated from one another.
- Persistent session state: session state can be restored even after server restarts.
For scenarios that need to handle a large number of concurrent requests, Orleans provides strong scalability.
Practical Guide
Section titled “Practical Guide”Configuration Example
Section titled “Configuration Example”Here is a complete configuration example:
{ "AI": { "Providers": { "Providers": { "GitHubCopilot": { "Enabled": true, "ExecutablePath": "copilot", "Model": "gpt-5", "WorkingDirectory": "/path/to/project", "Timeout": 7200, "StartupTimeout": 30, "UseLoggedInUser": true, "NoAskUser": true, "Permissions": { "AllowAllTools": false, "AllowedTools": ["Read", "Bash", "Grep"], "DeniedTools": ["Edit"] } } } } }}Usage Notes
Section titled “Usage Notes”In real-world usage, we summarized several points worth paying attention to:
Startup timeout configuration: The first startup of Copilot CLI can take a relatively long time, so we recommend setting StartupTimeout to at least 30 seconds. If this is the first login, it may take even longer.
Permission management: In production environments, avoid using AllowAllTools: true. Use the AllowedTools allowlist to control which tools are available, and use the DeniedTools denylist to block dangerous operations. This effectively prevents the AI from executing risky commands.
Session management: Requests with the same sessionId automatically reuse sessions. Session state is persisted through ProviderSessionId. Cancellation is propagated via CancellationTokenSource.
Diagnostic output: Incompatible CLI arguments generate messages of type diagnostic. Raw SDK events are preserved as event.raw. Error messages include categories such as startup timeout and argument incompatibility to make troubleshooting easier.
Best Practices
Section titled “Best Practices”Based on our practical experience, here are a few best practices:
1. Use a tool allowlist
var request = new AIRequest{ Prompt = "Analyze this file", AllowedTools = new[] { "Read", "Grep", "Bash(git:*)" }};Explicitly specifying the allowed tools through an allowlist helps prevent unexpected AI actions. This is especially important for tools with write permissions, such as Edit, which should be handled with extra care.
2. Set reasonable timeouts
options.Timeout = 3600; // 1 houroptions.StartupTimeout = 60; // 1 minuteSet appropriate timeout values based on task complexity. If the value is too short, tasks may be interrupted. If it is too long, resources may be wasted waiting on unresponsive requests.
3. Enable session reuse
options.SessionId = "my-session-123";Using the same sessionId for related tasks lets you reuse prior session context and improve response speed.
4. Handle streaming responses
await foreach (var chunk in provider.StreamAsync(request)){ switch (chunk.Type) { case StreamingChunkType.ThinkingDelta: // Handle the reasoning process break; case StreamingChunkType.ToolCallDelta: // Handle tool invocation break; case StreamingChunkType.ContentDelta: // Handle text output break; }}Streaming responses let you show AI processing progress in real time, which improves the user experience. This is especially valuable for long-running tasks.
5. Error handling and retries
try{ await foreach (var chunk in provider.StreamAsync(request)) { // Handle the response }}catch (CopilotSessionException ex){ // Handle session exceptions logger.LogError(ex, "Copilot session failed"); // Decide whether to retry based on the exception type}Proper error handling and retry logic improve overall system stability.
Conclusion
Section titled “Conclusion”Upgrading from CLI invocation to SDK integration delivered substantial value to the HagiCode project:
- Improved stability: the SDK provides a more stable interface that is not affected by CLI version changes.
- More complete functionality: it captures the full event stream, including reasoning and tool execution status.
- Higher development efficiency: the type-safe SDK interface makes development more efficient and reduces runtime errors.
- Better user experience: real-time event feedback gives users a clearer understanding of what the AI is doing.
This upgrade was not just a replacement of technical implementation. It was also an architectural optimization of the entire AI capability layer. Through layered design and abstraction interfaces, we gained better maintainability and extensibility.
If you are considering integrating GitHub Copilot into your own .NET project, I hope these practical lessons help you avoid unnecessary detours. The official SDK is indeed more stable and complete than CLI invocation, and it is worth the time to understand and adopt it.
References
Section titled “References”- Official GitHub Copilot SDK documentation
- Orleans distributed framework
- HagiCode project GitHub repository
- Official HagiCode documentation
- .NET dependency injection best practices
If this article helped you:
- Give it a like so more people can discover it.
- Star us on GitHub: github.com/HagiCode-org/site
- Visit the official website to learn more: hagicode.com
- Watch the 30-minute hands-on demo: www.bilibili.com/video/BV1pirZBuEzq/
- One-click installation experience: docs.hagicode.com/installation/docker-compose
- Quick installation for the Desktop app: hagicode.com/desktop/
- Public beta has started. You are welcome to install and try it out.
That brings this article to a close. Technical writing never really ends, because technology keeps evolving and we keep learning. If you have questions or suggestions while using HagiCode, feel free to contact us anytime.
Copyright Notice
Section titled “Copyright Notice”Thank you for reading. If you found this article useful, you are welcome to like, bookmark, and share it. This content was created with AI-assisted collaboration, and the final version was reviewed and approved by the author.
- Author: newbe36524
- Original link: https://docs.hagicode.com/blog/2026-04-03-github-copilot-sdk-integration/
- Copyright notice: Unless otherwise stated, all articles on this blog are licensed under BY-NC-SA. Please include the source when reposting.