.NET: Add AIAgent implementation for GitHub Copilot SDK (#3395)

* Initial plan

* Add GitHub Copilot SDK AIAgent implementation with tests

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Add projects to solution and fix sample imports

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Improve pragma comment clarity in GithubCopilotAgentThread

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Address PR feedback: internal constructor/setter, remove CopilotClientOptions ctor, streaming improvements, better sample, container warning

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Add ownsClient parameter to allow caller control over client disposal

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Fix unit tests by removing await using to avoid StreamJsonRpc disposal issues

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Fix file encoding: add UTF-8 BOM to Program.cs

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Fix dotnet-format errors: UTF-8 BOM, remove unused logger, add this qualifier, remove unnecessary usings

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Fix test file encoding and remove redundant cast

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Add AsAIAgent extension methods for CopilotClient with tests

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Remove IL suppressions, use TryComplete for channel writer, remove TCS from streaming

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Keep session alive across calls, add tools overload, add tests

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Revert session persistence changes - sessions dispose after each call

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Add CreatedAt property mapping using DateTimeOffset.UtcNow

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Add DataContent handling via temp files and attachments

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Fix formatting: remove extra indentation, simplify Path references, remove unused using

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Refactor: extract helper methods to reduce duplication in DataContent handling

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Updated sample and session config mapping

* Added instructions parameter

* Updated README

* Address PR feedback: reorder params, optimize dictionary, update prefix, remove InternalsVisibleTo, update sample prompts, add defaults

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Remove StreamJsonRpc reference from sample project

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Fix parameter ordering: tools now after description, rename to s_mediaTypeExtensions, simplify extension logic, update prompts, fix test expectations

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Fix streaming prompt: change Python to C# for Fibonacci example

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Handle all SDK events, add UsageContent support, fix model name, remove AutoStart, add using for Channels

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Resolved build errors

* Addressed comments

* Small fix

* Addressed comment

* Small fix

* Addressed comments

* Added integration tests

* Small update

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
This commit is contained in:
Copilot
2026-01-27 18:51:56 +00:00
committed by GitHub
Unverified
parent 968621e817
commit e82d9f5e45
16 changed files with 1300 additions and 0 deletions
+1
View File
@@ -89,6 +89,7 @@
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.67.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.67.0" />
<!-- Agent SDKs -->
<PackageVersion Include="GitHub.Copilot.SDK" Version="0.1.18" />
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.3.171-beta" />
<!-- M365 Agents SDK -->
<PackageVersion Include="AdaptiveCards" Version="3.1.0" />
+4
View File
@@ -65,6 +65,7 @@
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Agent_With_GithubCopilot.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj" />
@@ -395,6 +396,7 @@
<Project Path="src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj" />
<Project Path="src/Microsoft.Agents.AI.GithubCopilot/Microsoft.Agents.AI.GithubCopilot.csproj" />
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
<Project Path="src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
@@ -424,6 +426,7 @@
<Project Path="tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj" />
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests.csproj" />
@@ -438,6 +441,7 @@
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/Microsoft.Agents.AI.GithubCopilot.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AzureAI.UnitTests/Microsoft.Agents.AI.AzureAI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj" />
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GitHub.Copilot.SDK" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.GithubCopilot\Microsoft.Agents.AI.GithubCopilot.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to create a GitHub Copilot agent with shell command permissions.
using GitHub.Copilot.SDK;
using Microsoft.Agents.AI;
// Permission handler that prompts the user for approval
static Task<PermissionRequestResult> PromptPermission(PermissionRequest request, PermissionInvocation invocation)
{
Console.WriteLine($"\n[Permission Request: {request.Kind}]");
Console.Write("Approve? (y/n): ");
string? input = Console.ReadLine()?.Trim().ToUpperInvariant();
string kind = input is "Y" or "YES" ? "approved" : "denied-interactively-by-user";
return Task.FromResult(new PermissionRequestResult { Kind = kind });
}
// Create and start a Copilot client
await using CopilotClient copilotClient = new();
await copilotClient.StartAsync();
// Create an agent with a session config that enables permission handling
SessionConfig sessionConfig = new()
{
OnPermissionRequest = PromptPermission,
};
AIAgent agent = copilotClient.AsAIAgent(sessionConfig, ownsClient: true);
// Toggle between streaming and non-streaming modes
bool useStreaming = true;
string prompt = "List all files in the current directory";
Console.WriteLine($"User: {prompt}\n");
if (useStreaming)
{
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(prompt))
{
Console.Write(update);
}
Console.WriteLine();
}
else
{
AgentResponse response = await agent.RunAsync(prompt);
Console.WriteLine(response);
}
@@ -0,0 +1,76 @@
# Prerequisites
> **⚠️ WARNING: Container Recommendation**
>
> GitHub Copilot can execute tools and commands that may interact with your system. For safety, it is strongly recommended to run this sample in a containerized environment (e.g., Docker, Dev Container) to avoid unintended consequences to your machine.
Before you begin, ensure you have the following prerequisites:
- .NET 10 SDK or later
- GitHub Copilot CLI installed and available in your PATH (or provide a custom path)
## Setting up GitHub Copilot CLI
To use this sample, you need to have the GitHub Copilot CLI installed. You can install it by following the instructions at:
https://github.com/github/copilot-sdk
Once installed, ensure the `copilot` command is available in your PATH, or configure a custom path using `CopilotClientOptions`.
## Running the Sample
No additional environment variables are required if using default configuration. The sample will:
1. Create a GitHub Copilot client with default options
2. Create an AI agent using the Copilot SDK
3. Send a message to the agent
4. Display the response
Run the sample:
```powershell
dotnet run
```
## Advanced Usage
You can customize the agent by providing additional configuration:
```csharp
using GitHub.Copilot.SDK;
using Microsoft.Agents.AI;
// Create and start a Copilot client
await using CopilotClient copilotClient = new();
await copilotClient.StartAsync();
// Create session configuration with specific model
SessionConfig sessionConfig = new()
{
Model = "claude-opus-4.5",
Streaming = false
};
// Create an agent with custom configuration using the extension method
AIAgent agent = copilotClient.AsAIAgent(
sessionConfig,
ownsClient: true,
id: "my-copilot-agent",
name: "My Copilot Assistant",
description: "A helpful AI assistant powered by GitHub Copilot"
);
// Use the agent - ask it to write code for us
AgentResponse response = await agent.RunAsync("Write a small .NET 10 C# hello world single file application");
Console.WriteLine(response);
```
## Streaming Responses
To get streaming responses:
```csharp
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("Write a C# function to calculate Fibonacci numbers"))
{
Console.Write(update.Text);
}
```
@@ -22,6 +22,7 @@ See the README.md for each sample for the prerequisites for that sample.
|[Creating an AIAgent with Azure OpenAI ChatCompletion](./Agent_With_AzureOpenAIChatCompletion/)|This sample demonstrates how to create an AIAgent using Azure OpenAI ChatCompletion as the underlying inference service|
|[Creating an AIAgent with Azure OpenAI Responses](./Agent_With_AzureOpenAIResponses/)|This sample demonstrates how to create an AIAgent using Azure OpenAI Responses as the underlying inference service|
|[Creating an AIAgent with a custom implementation](./Agent_With_CustomImplementation/)|This sample demonstrates how to create an AIAgent with a custom implementation|
|[Creating an AIAgent with GitHub Copilot](./Agent_With_GithubCopilot/)|This sample demonstrates how to create an AIAgent using GitHub Copilot SDK as the underlying inference service|
|[Creating an AIAgent with Ollama](./Agent_With_Ollama/)|This sample demonstrates how to create an AIAgent using Ollama as the underlying inference service|
|[Creating an AIAgent with ONNX](./Agent_With_ONNX/)|This sample demonstrates how to create an AIAgent using ONNX as the underlying inference service|
|[Creating an AIAgent with OpenAI Assistants](./Agent_With_OpenAIAssistants/)|This sample demonstrates how to create an AIAgent using OpenAI Assistants as the underlying inference service.</br>WARNING: The Assistants API is deprecated and will be shut down. For more information see the OpenAI documentation: https://platform.openai.com/docs/assistants/migration|
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.GithubCopilot;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace GitHub.Copilot.SDK;
/// <summary>
/// Provides extension methods for <see cref="CopilotClient"/>
/// to simplify the creation of GitHub Copilot agents.
/// </summary>
/// <remarks>
/// These extensions bridge the gap between GitHub Copilot SDK client objects
/// and the Microsoft Agent Framework.
/// <para>
/// They allow developers to easily create AI agents that can interact
/// with GitHub Copilot by handling the conversion from Copilot clients to
/// <see cref="GithubCopilotAgent"/> instances that implement the <see cref="AIAgent"/> interface.
/// </para>
/// </remarks>
public static class CopilotClientExtensions
{
/// <summary>
/// Retrieves an instance of <see cref="AIAgent"/> for a GitHub Copilot client.
/// </summary>
/// <param name="client">The <see cref="CopilotClient"/> to use for the agent.</param>
/// <param name="sessionConfig">Optional session configuration for the agent.</param>
/// <param name="ownsClient">Whether the agent owns the client and should dispose it. Default is false.</param>
/// <param name="id">The unique identifier for the agent.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="description">The description of the agent.</param>
/// <returns>An <see cref="AIAgent"/> instance backed by the GitHub Copilot client.</returns>
public static AIAgent AsAIAgent(
this CopilotClient client,
SessionConfig? sessionConfig = null,
bool ownsClient = false,
string? id = null,
string? name = null,
string? description = null)
{
Throw.IfNull(client);
return new GithubCopilotAgent(client, sessionConfig, ownsClient, id, name, description);
}
/// <summary>
/// Retrieves an instance of <see cref="AIAgent"/> for a GitHub Copilot client.
/// </summary>
/// <param name="client">The <see cref="CopilotClient"/> to use for the agent.</param>
/// <param name="ownsClient">Whether the agent owns the client and should dispose it. Default is false.</param>
/// <param name="id">The unique identifier for the agent.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="description">The description of the agent.</param>
/// <param name="tools">The tools to make available to the agent.</param>
/// <param name="instructions">Optional instructions to append as a system message.</param>
/// <returns>An <see cref="AIAgent"/> instance backed by the GitHub Copilot client.</returns>
public static AIAgent AsAIAgent(
this CopilotClient client,
bool ownsClient = false,
string? id = null,
string? name = null,
string? description = null,
IList<AITool>? tools = null,
string? instructions = null)
{
Throw.IfNull(client);
return new GithubCopilotAgent(client, ownsClient, id, name, description, tools, instructions);
}
}
@@ -0,0 +1,470 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using GitHub.Copilot.SDK;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.GithubCopilot;
/// <summary>
/// Represents an <see cref="AIAgent"/> that uses the GitHub Copilot SDK to provide agentic capabilities.
/// </summary>
public sealed class GithubCopilotAgent : AIAgent, IAsyncDisposable
{
private const string DefaultName = "GitHub Copilot Agent";
private const string DefaultDescription = "An AI agent powered by GitHub Copilot";
private readonly CopilotClient _copilotClient;
private readonly string? _id;
private readonly string _name;
private readonly string _description;
private readonly SessionConfig? _sessionConfig;
private readonly bool _ownsClient;
/// <summary>
/// Initializes a new instance of the <see cref="GithubCopilotAgent"/> class.
/// </summary>
/// <param name="copilotClient">The Copilot client to use for interacting with GitHub Copilot.</param>
/// <param name="sessionConfig">Optional session configuration for the agent.</param>
/// <param name="ownsClient">Whether the agent owns the client and should dispose it. Default is false.</param>
/// <param name="id">The unique identifier for the agent.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="description">The description of the agent.</param>
public GithubCopilotAgent(
CopilotClient copilotClient,
SessionConfig? sessionConfig = null,
bool ownsClient = false,
string? id = null,
string? name = null,
string? description = null)
{
_ = Throw.IfNull(copilotClient);
this._copilotClient = copilotClient;
this._sessionConfig = sessionConfig;
this._ownsClient = ownsClient;
this._id = id;
this._name = name ?? DefaultName;
this._description = description ?? DefaultDescription;
}
/// <summary>
/// Initializes a new instance of the <see cref="GithubCopilotAgent"/> class.
/// </summary>
/// <param name="copilotClient">The Copilot client to use for interacting with GitHub Copilot.</param>
/// <param name="ownsClient">Whether the agent owns the client and should dispose it. Default is false.</param>
/// <param name="id">The unique identifier for the agent.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="description">The description of the agent.</param>
/// <param name="tools">The tools to make available to the agent.</param>
/// <param name="instructions">Optional instructions to append as a system message.</param>
public GithubCopilotAgent(
CopilotClient copilotClient,
bool ownsClient = false,
string? id = null,
string? name = null,
string? description = null,
IList<AITool>? tools = null,
string? instructions = null)
: this(
copilotClient,
GetSessionConfig(tools, instructions),
ownsClient,
id,
name,
description)
{
}
/// <inheritdoc/>
public sealed override ValueTask<AgentSession> GetNewSessionAsync(CancellationToken cancellationToken = default)
=> new(new GithubCopilotAgentSession());
/// <summary>
/// Get a new <see cref="AgentSession"/> instance using an existing session id, to continue that conversation.
/// </summary>
/// <param name="sessionId">The session id to continue.</param>
/// <returns>A new <see cref="AgentSession"/> instance.</returns>
public ValueTask<AgentSession> GetNewSessionAsync(string sessionId)
=> new(new GithubCopilotAgentSession() { SessionId = sessionId });
/// <inheritdoc/>
public override ValueTask<AgentSession> DeserializeSessionAsync(
JsonElement serializedSession,
JsonSerializerOptions? jsonSerializerOptions = null,
CancellationToken cancellationToken = default)
=> new(new GithubCopilotAgentSession(serializedSession, jsonSerializerOptions));
/// <inheritdoc/>
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
=> this.RunCoreStreamingAsync(messages, session, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
/// <inheritdoc/>
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(messages);
// Ensure we have a valid session
session ??= await this.GetNewSessionAsync(cancellationToken).ConfigureAwait(false);
if (session is not GithubCopilotAgentSession typedSession)
{
throw new InvalidOperationException(
$"The provided session type {session.GetType()} is not compatible with the agent. Only GitHub Copilot agent created sessions are supported.");
}
// Ensure the client is started
await this.EnsureClientStartedAsync(cancellationToken).ConfigureAwait(false);
// Create or resume a session with streaming enabled
SessionConfig sessionConfig = this._sessionConfig != null
? new SessionConfig
{
Model = this._sessionConfig.Model,
Tools = this._sessionConfig.Tools,
SystemMessage = this._sessionConfig.SystemMessage,
AvailableTools = this._sessionConfig.AvailableTools,
ExcludedTools = this._sessionConfig.ExcludedTools,
Provider = this._sessionConfig.Provider,
OnPermissionRequest = this._sessionConfig.OnPermissionRequest,
McpServers = this._sessionConfig.McpServers,
CustomAgents = this._sessionConfig.CustomAgents,
SkillDirectories = this._sessionConfig.SkillDirectories,
DisabledSkills = this._sessionConfig.DisabledSkills,
Streaming = true
}
: new SessionConfig { Streaming = true };
CopilotSession copilotSession;
if (typedSession.SessionId is not null)
{
copilotSession = await this._copilotClient.ResumeSessionAsync(
typedSession.SessionId,
this.CreateResumeConfig(),
cancellationToken).ConfigureAwait(false);
}
else
{
copilotSession = await this._copilotClient.CreateSessionAsync(sessionConfig, cancellationToken).ConfigureAwait(false);
typedSession.SessionId = copilotSession.SessionId;
}
try
{
Channel<AgentResponseUpdate> channel = Channel.CreateUnbounded<AgentResponseUpdate>();
// Subscribe to session events
using IDisposable subscription = copilotSession.On(evt =>
{
switch (evt)
{
case AssistantMessageDeltaEvent deltaEvent:
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(deltaEvent));
break;
case AssistantMessageEvent assistantMessage:
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(assistantMessage));
break;
case AssistantUsageEvent usageEvent:
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(usageEvent));
break;
case SessionIdleEvent idleEvent:
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(idleEvent));
channel.Writer.TryComplete();
break;
case SessionErrorEvent errorEvent:
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(errorEvent));
channel.Writer.TryComplete(new InvalidOperationException(
$"Session error: {errorEvent.Data?.Message ?? "Unknown error"}"));
break;
default:
// Handle all other event types by storing as RawRepresentation
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(evt));
break;
}
});
List<string> tempFiles = [];
try
{
// Build prompt from text content
string prompt = string.Join("\n", messages.Select(m => m.Text));
// Handle DataContent as attachments
List<UserMessageDataAttachmentsItem>? attachments = await ProcessDataContentAttachmentsAsync(
messages,
tempFiles,
cancellationToken).ConfigureAwait(false);
// Send the message with attachments
MessageOptions messageOptions = new() { Prompt = prompt };
if (attachments is not null)
{
messageOptions.Attachments = [.. attachments];
}
await copilotSession.SendAsync(messageOptions, cancellationToken).ConfigureAwait(false);
// Yield updates as they arrive
await foreach (AgentResponseUpdate update in channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
{
yield return update;
}
}
finally
{
CleanupTempFiles(tempFiles);
}
}
finally
{
await copilotSession.DisposeAsync().ConfigureAwait(false);
}
}
/// <inheritdoc/>
protected override string? IdCore => this._id;
/// <inheritdoc/>
public override string Name => this._name;
/// <inheritdoc/>
public override string Description => this._description;
/// <summary>
/// Disposes the agent and releases resources.
/// </summary>
/// <returns>A value task representing the asynchronous dispose operation.</returns>
public async ValueTask DisposeAsync()
{
if (this._ownsClient)
{
await this._copilotClient.DisposeAsync().ConfigureAwait(false);
}
}
private async Task EnsureClientStartedAsync(CancellationToken cancellationToken)
{
if (this._copilotClient.State != ConnectionState.Connected)
{
await this._copilotClient.StartAsync(cancellationToken).ConfigureAwait(false);
}
}
private ResumeSessionConfig CreateResumeConfig()
{
return new ResumeSessionConfig
{
Tools = this._sessionConfig?.Tools,
Provider = this._sessionConfig?.Provider,
OnPermissionRequest = this._sessionConfig?.OnPermissionRequest,
McpServers = this._sessionConfig?.McpServers,
CustomAgents = this._sessionConfig?.CustomAgents,
SkillDirectories = this._sessionConfig?.SkillDirectories,
DisabledSkills = this._sessionConfig?.DisabledSkills,
Streaming = true
};
}
private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageDeltaEvent deltaEvent)
{
TextContent textContent = new(deltaEvent.Data?.DeltaContent ?? string.Empty)
{
RawRepresentation = deltaEvent
};
return new AgentResponseUpdate(ChatRole.Assistant, [textContent])
{
AgentId = this.Id,
MessageId = deltaEvent.Data?.MessageId,
CreatedAt = deltaEvent.Timestamp
};
}
private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent assistantMessage)
{
TextContent textContent = new(assistantMessage.Data?.Content ?? string.Empty)
{
RawRepresentation = assistantMessage
};
return new AgentResponseUpdate(ChatRole.Assistant, [textContent])
{
AgentId = this.Id,
ResponseId = assistantMessage.Data?.MessageId,
MessageId = assistantMessage.Data?.MessageId,
CreatedAt = assistantMessage.Timestamp
};
}
private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantUsageEvent usageEvent)
{
UsageDetails usageDetails = new()
{
InputTokenCount = (int?)(usageEvent.Data?.InputTokens),
OutputTokenCount = (int?)(usageEvent.Data?.OutputTokens),
TotalTokenCount = (int?)((usageEvent.Data?.InputTokens ?? 0) + (usageEvent.Data?.OutputTokens ?? 0)),
CachedInputTokenCount = (int?)(usageEvent.Data?.CacheReadTokens),
AdditionalCounts = GetAdditionalCounts(usageEvent),
};
UsageContent usageContent = new(usageDetails)
{
RawRepresentation = usageEvent
};
return new AgentResponseUpdate(ChatRole.Assistant, [usageContent])
{
AgentId = this.Id,
CreatedAt = usageEvent.Timestamp
};
}
private static AdditionalPropertiesDictionary<long>? GetAdditionalCounts(AssistantUsageEvent usageEvent)
{
if (usageEvent.Data is null)
{
return null;
}
AdditionalPropertiesDictionary<long>? additionalCounts = null;
if (usageEvent.Data.CacheWriteTokens is double cacheWriteTokens)
{
additionalCounts ??= [];
additionalCounts[nameof(AssistantUsageData.CacheWriteTokens)] = (long)cacheWriteTokens;
}
if (usageEvent.Data.Cost is double cost)
{
additionalCounts ??= [];
additionalCounts[nameof(AssistantUsageData.Cost)] = (long)cost;
}
if (usageEvent.Data.Duration is double duration)
{
additionalCounts ??= [];
additionalCounts[nameof(AssistantUsageData.Duration)] = (long)duration;
}
return additionalCounts;
}
private AgentResponseUpdate ConvertToAgentResponseUpdate(SessionEvent sessionEvent)
{
// Handle arbitrary events by storing as RawRepresentation
AIContent content = new()
{
RawRepresentation = sessionEvent
};
return new AgentResponseUpdate(ChatRole.Assistant, [content])
{
AgentId = this.Id,
CreatedAt = sessionEvent.Timestamp
};
}
private static SessionConfig? GetSessionConfig(IList<AITool>? tools, string? instructions)
{
List<AIFunction>? mappedTools = tools is { Count: > 0 } ? tools.OfType<AIFunction>().ToList() : null;
SystemMessageConfig? systemMessage = instructions is not null ? new SystemMessageConfig { Mode = SystemMessageMode.Append, Content = instructions } : null;
if (mappedTools is null && systemMessage is null)
{
return null;
}
return new SessionConfig { Tools = mappedTools, SystemMessage = systemMessage };
}
private static readonly Dictionary<string, string> s_mediaTypeExtensions = new(StringComparer.OrdinalIgnoreCase)
{
["image/png"] = ".png",
["image/jpeg"] = ".jpg",
["image/jpg"] = ".jpg",
["image/gif"] = ".gif",
["image/webp"] = ".webp",
["image/svg+xml"] = ".svg",
["text/plain"] = ".txt",
["text/html"] = ".html",
["text/markdown"] = ".md",
["application/json"] = ".json",
["application/xml"] = ".xml",
["application/pdf"] = ".pdf"
};
private static string GetExtensionForMediaType(string? mediaType)
{
return mediaType is not null && s_mediaTypeExtensions.TryGetValue(mediaType, out string? extension) ? extension : ".dat";
}
private static async Task<List<UserMessageDataAttachmentsItem>?> ProcessDataContentAttachmentsAsync(
IEnumerable<ChatMessage> messages,
List<string> tempFiles,
CancellationToken cancellationToken)
{
List<UserMessageDataAttachmentsItem>? attachments = null;
foreach (ChatMessage message in messages)
{
foreach (AIContent content in message.Contents)
{
if (content is DataContent dataContent)
{
// Write DataContent to a temp file
string tempFilePath = Path.Combine(Path.GetTempPath(), $"agentframework_copilot_data_{Guid.NewGuid()}{GetExtensionForMediaType(dataContent.MediaType)}");
await File.WriteAllBytesAsync(tempFilePath, dataContent.Data.ToArray(), cancellationToken).ConfigureAwait(false);
tempFiles.Add(tempFilePath);
// Create attachment
attachments ??= [];
attachments.Add(new UserMessageDataAttachmentsItem
{
Type = UserMessageDataAttachmentsItemType.File,
Path = tempFilePath,
DisplayName = Path.GetFileName(tempFilePath)
});
}
}
}
return attachments;
}
private static void CleanupTempFiles(List<string> tempFiles)
{
foreach (string tempFile in tempFiles)
{
try
{
if (File.Exists(tempFile))
{
File.Delete(tempFile);
}
}
catch
{
// Best effort cleanup
}
}
}
}
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
namespace Microsoft.Agents.AI.GithubCopilot;
/// <summary>
/// Represents a session for a GitHub Copilot agent conversation.
/// </summary>
public sealed class GithubCopilotAgentSession : AgentSession
{
/// <summary>
/// Gets or sets the session ID for the GitHub Copilot conversation.
/// </summary>
public string? SessionId { get; internal set; }
/// <summary>
/// Initializes a new instance of the <see cref="GithubCopilotAgentSession"/> class.
/// </summary>
internal GithubCopilotAgentSession()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="GithubCopilotAgentSession"/> class from serialized data.
/// </summary>
/// <param name="serializedThread">The serialized thread data.</param>
/// <param name="jsonSerializerOptions">Optional JSON serialization options.</param>
internal GithubCopilotAgentSession(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
{
// The JSON serialization uses camelCase
if (serializedThread.TryGetProperty("sessionId", out JsonElement sessionIdElement))
{
this.SessionId = sessionIdElement.GetString();
}
}
/// <inheritdoc/>
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
State state = new()
{
SessionId = this.SessionId
};
return JsonSerializer.SerializeToElement(
state,
GithubCopilotJsonUtilities.DefaultOptions.GetTypeInfo(typeof(State)));
}
internal sealed class State
{
public string? SessionId { get; set; }
}
}
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI.GithubCopilot;
/// <summary>
/// Provides utility methods and configurations for JSON serialization operations within the GitHub Copilot agent implementation.
/// </summary>
internal static partial class GithubCopilotJsonUtilities
{
/// <summary>
/// Gets the default <see cref="JsonSerializerOptions"/> instance used for JSON serialization operations.
/// </summary>
public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions();
/// <summary>
/// Creates and configures the default JSON serialization options.
/// </summary>
/// <returns>The configured options.</returns>
private static JsonSerializerOptions CreateDefaultOptions()
{
// Copy the configuration from the source generated context.
JsonSerializerOptions options = new(JsonContext.Default.Options)
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
// Chain in the resolvers from both AgentAbstractionsJsonUtilities and our source generated context.
options.TypeInfoResolverChain.Clear();
options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!);
options.TypeInfoResolverChain.Add(JsonContext.Default.Options.TypeInfoResolver!);
options.MakeReadOnly();
return options;
}
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
UseStringEnumConverter = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString)]
[JsonSerializable(typeof(GithubCopilotAgentSession.State))]
[ExcludeFromCodeCoverage]
private sealed partial class JsonContext : JsonSerializerContext;
}
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<VersionSuffix>preview</VersionSuffix>
<!-- GitHub.Copilot.SDK only supports .NET 8.0+ -->
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
<InjectDiagnosticClassesOnLegacy>true</InjectDiagnosticClassesOnLegacy>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<ItemGroup>
<ProjectReference Include="..\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GitHub.Copilot.SDK" />
</ItemGroup>
<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Microsoft Agent Framework GitHub Copilot</Title>
<Description>Provides Microsoft Agent Framework support for GitHub Copilot SDK.</Description>
</PropertyGroup>
</Project>
@@ -0,0 +1,257 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using GitHub.Copilot.SDK;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.GithubCopilot.IntegrationTests;
public class GithubCopilotAgentTests
{
private const string SkipReason = "Integration tests require GitHub Copilot CLI installed. For local execution only.";
private static Task<PermissionRequestResult> OnPermissionRequestAsync(PermissionRequest request, PermissionInvocation invocation)
=> Task.FromResult(new PermissionRequestResult { Kind = "approved" });
[Fact(Skip = SkipReason)]
public async Task RunAsync_WithSimplePrompt_ReturnsResponseAsync()
{
// Arrange
await using CopilotClient client = new(new CopilotClientOptions());
await client.StartAsync();
await using GithubCopilotAgent agent = new(client, sessionConfig: null);
// Act
AgentResponse response = await agent.RunAsync("What is 2 + 2? Answer with just the number.");
// Assert
Assert.NotNull(response);
Assert.NotEmpty(response.Messages);
Assert.Contains("4", response.Text);
}
[Fact(Skip = SkipReason)]
public async Task RunStreamingAsync_WithSimplePrompt_ReturnsUpdatesAsync()
{
// Arrange
await using CopilotClient client = new(new CopilotClientOptions());
await client.StartAsync();
await using GithubCopilotAgent agent = new(client, sessionConfig: null);
// Act
List<AgentResponseUpdate> updates = [];
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("What is 2 + 2? Answer with just the number."))
{
updates.Add(update);
}
// Assert
Assert.NotEmpty(updates);
string fullText = string.Join("", updates.Select(u => u.Text));
Assert.Contains("4", fullText);
}
[Fact(Skip = SkipReason)]
public async Task RunAsync_WithFunctionTool_InvokesToolAsync()
{
// Arrange
bool toolInvoked = false;
AIFunction weatherTool = AIFunctionFactory.Create((string location) =>
{
toolInvoked = true;
return $"The weather in {location} is sunny with a high of 25C.";
}, "GetWeather", "Get the weather for a given location.");
await using CopilotClient client = new(new CopilotClientOptions());
await client.StartAsync();
await using GithubCopilotAgent agent = new(
client,
tools: [weatherTool],
instructions: "You are a helpful weather agent. Use the GetWeather tool to answer weather questions.");
// Act
AgentResponse response = await agent.RunAsync("What's the weather like in Seattle?");
// Assert
Assert.NotNull(response);
Assert.NotEmpty(response.Messages);
Assert.True(toolInvoked);
}
[Fact(Skip = SkipReason)]
public async Task RunAsync_WithSession_MaintainsContextAsync()
{
// Arrange
await using CopilotClient client = new(new CopilotClientOptions());
await client.StartAsync();
await using GithubCopilotAgent agent = new(
client,
instructions: "You are a helpful assistant. Keep your answers short.");
AgentSession session = await agent.GetNewSessionAsync();
// Act - First turn
AgentResponse response1 = await agent.RunAsync("My name is Alice.", session);
Assert.NotNull(response1);
// Act - Second turn using same session
AgentResponse response2 = await agent.RunAsync("What is my name?", session);
// Assert
Assert.NotNull(response2);
Assert.Contains("Alice", response2.Text, StringComparison.OrdinalIgnoreCase);
}
[Fact(Skip = SkipReason)]
public async Task RunAsync_WithSessionResume_ContinuesConversationAsync()
{
// Arrange - First agent instance starts a conversation
string? sessionId;
await using CopilotClient client1 = new(new CopilotClientOptions());
await client1.StartAsync();
await using GithubCopilotAgent agent1 = new(
client1,
instructions: "You are a helpful assistant. Keep your answers short.");
AgentSession session1 = await agent1.GetNewSessionAsync();
await agent1.RunAsync("Remember this number: 42.", session1);
sessionId = ((GithubCopilotAgentSession)session1).SessionId;
Assert.NotNull(sessionId);
// Act - Second agent instance resumes the session
await using CopilotClient client2 = new(new CopilotClientOptions());
await client2.StartAsync();
await using GithubCopilotAgent agent2 = new(
client2,
instructions: "You are a helpful assistant. Keep your answers short.");
AgentSession session2 = await agent2.GetNewSessionAsync(sessionId);
AgentResponse response = await agent2.RunAsync("What number did I ask you to remember?", session2);
// Assert
Assert.NotNull(response);
Assert.Contains("42", response.Text);
}
[Fact(Skip = SkipReason)]
public async Task RunAsync_WithShellPermissions_ExecutesCommandAsync()
{
// Arrange
await using CopilotClient client = new(new CopilotClientOptions());
await client.StartAsync();
SessionConfig sessionConfig = new()
{
OnPermissionRequest = OnPermissionRequestAsync,
};
await using GithubCopilotAgent agent = new(client, sessionConfig);
// Act
AgentResponse response = await agent.RunAsync("Run a shell command to print 'hello world'");
// Assert
Assert.NotNull(response);
Assert.NotEmpty(response.Messages);
Assert.Contains("hello", response.Text, StringComparison.OrdinalIgnoreCase);
}
[Fact(Skip = SkipReason)]
public async Task RunAsync_WithUrlPermissions_FetchesContentAsync()
{
// Arrange
await using CopilotClient client = new(new CopilotClientOptions());
await client.StartAsync();
SessionConfig sessionConfig = new()
{
OnPermissionRequest = OnPermissionRequestAsync,
};
await using GithubCopilotAgent agent = new(client, sessionConfig);
// Act
AgentResponse response = await agent.RunAsync(
"Fetch https://learn.microsoft.com/agent-framework/tutorials/quick-start and summarize its contents in one sentence");
// Assert
Assert.NotNull(response);
Assert.Contains("Agent Framework", response.Text, StringComparison.OrdinalIgnoreCase);
}
[Fact(Skip = SkipReason)]
public async Task RunAsync_WithLocalMcpServer_UsesServerToolsAsync()
{
// Arrange
await using CopilotClient client = new(new CopilotClientOptions());
await client.StartAsync();
SessionConfig sessionConfig = new()
{
OnPermissionRequest = OnPermissionRequestAsync,
McpServers = new Dictionary<string, object>
{
["filesystem"] = new McpLocalServerConfig
{
Type = "stdio",
Command = "npx",
Args = ["-y", "@modelcontextprotocol/server-filesystem", "."],
Tools = ["*"],
},
},
};
await using GithubCopilotAgent agent = new(client, sessionConfig);
// Act
AgentResponse response = await agent.RunAsync("List the files in the current directory");
// Assert
Assert.NotNull(response);
Assert.NotEmpty(response.Messages);
Assert.NotEmpty(response.Text);
}
[Fact(Skip = SkipReason)]
public async Task RunAsync_WithRemoteMcpServer_UsesServerToolsAsync()
{
// Arrange
await using CopilotClient client = new(new CopilotClientOptions());
await client.StartAsync();
SessionConfig sessionConfig = new()
{
OnPermissionRequest = OnPermissionRequestAsync,
McpServers = new Dictionary<string, object>
{
["microsoft-learn"] = new McpRemoteServerConfig
{
Type = "http",
Url = "https://learn.microsoft.com/api/mcp",
Tools = ["*"],
},
},
};
await using GithubCopilotAgent agent = new(client, sessionConfig);
// Act
AgentResponse response = await agent.RunAsync("Search Microsoft Learn for 'Azure Functions' and summarize the top result");
// Assert
Assert.NotNull(response);
Assert.Contains("Azure Functions", response.Text, StringComparison.OrdinalIgnoreCase);
}
}
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- GitHub.Copilot.SDK only supports .NET 8.0+ -->
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.GithubCopilot\Microsoft.Agents.AI.GithubCopilot.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,88 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using GitHub.Copilot.SDK;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.GithubCopilot.UnitTests;
/// <summary>
/// Unit tests for the <see cref="CopilotClientExtensions"/> class.
/// </summary>
public sealed class CopilotClientExtensionsTests
{
[Fact]
public void AsAIAgent_WithAllParameters_ReturnsGithubCopilotAgentWithSpecifiedProperties()
{
// Arrange
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
const string TestId = "test-agent-id";
const string TestName = "Test Agent";
const string TestDescription = "This is a test agent description";
// Act
var agent = copilotClient.AsAIAgent(ownsClient: false, id: TestId, name: TestName, description: TestDescription, tools: null);
// Assert
Assert.NotNull(agent);
Assert.IsType<GithubCopilotAgent>(agent);
Assert.Equal(TestId, agent.Id);
Assert.Equal(TestName, agent.Name);
Assert.Equal(TestDescription, agent.Description);
}
[Fact]
public void AsAIAgent_WithMinimalParameters_ReturnsGithubCopilotAgent()
{
// Arrange
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
// Act
var agent = copilotClient.AsAIAgent(ownsClient: false, tools: null);
// Assert
Assert.NotNull(agent);
Assert.IsType<GithubCopilotAgent>(agent);
}
[Fact]
public void AsAIAgent_WithNullClient_ThrowsArgumentNullException()
{
// Arrange
CopilotClient? copilotClient = null;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => copilotClient!.AsAIAgent(sessionConfig: null));
}
[Fact]
public void AsAIAgent_WithOwnsClient_ReturnsAgentThatOwnsClient()
{
// Arrange
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
// Act
var agent = copilotClient.AsAIAgent(ownsClient: true, tools: null);
// Assert
Assert.NotNull(agent);
Assert.IsType<GithubCopilotAgent>(agent);
}
[Fact]
public void AsAIAgent_WithTools_ReturnsAgentWithTools()
{
// Arrange
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
List<AITool> tools = [AIFunctionFactory.Create(() => "test", "TestFunc", "Test function")];
// Act
var agent = copilotClient.AsAIAgent(tools: tools);
// Assert
Assert.NotNull(agent);
Assert.IsType<GithubCopilotAgent>(agent);
}
}
@@ -0,0 +1,103 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using GitHub.Copilot.SDK;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.GithubCopilot.UnitTests;
/// <summary>
/// Unit tests for the <see cref="GithubCopilotAgent"/> class.
/// </summary>
public sealed class GithubCopilotAgentTests
{
[Fact]
public void Constructor_WithCopilotClient_InitializesPropertiesCorrectly()
{
// Arrange
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
const string TestId = "test-id";
const string TestName = "test-name";
const string TestDescription = "test-description";
// Act
var agent = new GithubCopilotAgent(copilotClient, ownsClient: false, id: TestId, name: TestName, description: TestDescription, tools: null);
// Assert
Assert.Equal(TestId, agent.Id);
Assert.Equal(TestName, agent.Name);
Assert.Equal(TestDescription, agent.Description);
}
[Fact]
public void Constructor_WithNullCopilotClient_ThrowsArgumentNullException()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new GithubCopilotAgent(copilotClient: null!, sessionConfig: null));
}
[Fact]
public void Constructor_WithDefaultParameters_UsesBaseProperties()
{
// Arrange
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
// Act
var agent = new GithubCopilotAgent(copilotClient, ownsClient: false, tools: null);
// Assert
Assert.NotNull(agent.Id);
Assert.NotEmpty(agent.Id);
Assert.Equal("GitHub Copilot Agent", agent.Name);
Assert.Equal("An AI agent powered by GitHub Copilot", agent.Description);
}
[Fact]
public async Task GetNewSessionAsync_ReturnsGithubCopilotAgentSessionAsync()
{
// Arrange
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
var agent = new GithubCopilotAgent(copilotClient, ownsClient: false, tools: null);
// Act
var session = await agent.GetNewSessionAsync();
// Assert
Assert.NotNull(session);
Assert.IsType<GithubCopilotAgentSession>(session);
}
[Fact]
public async Task GetNewSessionAsync_WithSessionId_ReturnsSessionWithSessionIdAsync()
{
// Arrange
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
var agent = new GithubCopilotAgent(copilotClient, ownsClient: false, tools: null);
const string TestSessionId = "test-session-id";
// Act
var session = await agent.GetNewSessionAsync(TestSessionId);
// Assert
Assert.NotNull(session);
var typedSession = Assert.IsType<GithubCopilotAgentSession>(session);
Assert.Equal(TestSessionId, typedSession.SessionId);
}
[Fact]
public void Constructor_WithTools_InitializesCorrectly()
{
// Arrange
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
List<AITool> tools = [AIFunctionFactory.Create(() => "test", "TestFunc", "Test function")];
// Act
var agent = new GithubCopilotAgent(copilotClient, tools: tools);
// Assert
Assert.NotNull(agent);
Assert.NotNull(agent.Id);
}
}
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- GitHub.Copilot.SDK only supports .NET 8.0+ -->
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.GithubCopilot\Microsoft.Agents.AI.GithubCopilot.csproj" />
</ItemGroup>
</Project>