From 4c55bd9ba920a24edff19776ff6aa586e9408841 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 20:56:17 +0000 Subject: [PATCH 01/34] Initial plan From 5a39df4340c432b274205c4e695572c7bc53b233 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:10:52 +0000 Subject: [PATCH 02/34] Add GitHub Copilot SDK AIAgent implementation with tests Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> --- dotnet/Directory.Packages.props | 1 + .../Agent_With_GithubCopilot.csproj | 19 + .../Agent_With_GithubCopilot/Program.cs | 20 + .../Agent_With_GithubCopilot/README.md | 77 ++++ .../GettingStarted/AgentProviders/README.md | 1 + .../GithubCopilotAgent.cs | 348 ++++++++++++++++++ .../GithubCopilotAgentThread.cs | 60 +++ .../GithubCopilotJsonUtilities.cs | 57 +++ .../Microsoft.Agents.AI.GithubCopilot.csproj | 34 ++ .../GithubCopilotAgentTests.cs | 93 +++++ .../GithubCopilotAgentThreadTests.cs | 83 +++++ ...t.Agents.AI.GithubCopilot.UnitTests.csproj | 12 + 12 files changed, 805 insertions(+) create mode 100644 dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Agent_With_GithubCopilot.csproj create mode 100644 dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs create mode 100644 dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md create mode 100644 dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.GithubCopilot/Microsoft.Agents.AI.GithubCopilot.csproj create mode 100644 dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/Microsoft.Agents.AI.GithubCopilot.UnitTests.csproj diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index d721e208ff..04e1ba6441 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -89,6 +89,7 @@ + diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Agent_With_GithubCopilot.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Agent_With_GithubCopilot.csproj new file mode 100644 index 0000000000..69d7086d5e --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Agent_With_GithubCopilot.csproj @@ -0,0 +1,19 @@ + + + + Exe + net10.0 + + enable + enable + + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs new file mode 100644 index 0000000000..4af23f1875 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to create and use a simple AI agent with GitHub Copilot SDK. + +using GitHub.Copilot.SDK; +using Microsoft.Agents.AI.GithubCopilot; +using Microsoft.Extensions.AI; + +// Create a Copilot client with default options +var copilotClientOptions = new CopilotClientOptions +{ + AutoStart = true +}; + +// Create an instance of the AIAgent using GitHub Copilot SDK +AIAgent agent = new GithubCopilotAgent(copilotClientOptions); + +// Invoke the agent and output the text result +AgentResponse response = await agent.RunAsync("Tell me a joke about a pirate."); +Console.WriteLine(response); diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md new file mode 100644 index 0000000000..3283873d69 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md @@ -0,0 +1,77 @@ +# Prerequisites + +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.GithubCopilot; +using Microsoft.Extensions.AI; + +// Create a Copilot client with custom options +var copilotClientOptions = new CopilotClientOptions +{ + CliPath = "/custom/path/to/copilot", // Custom CLI path + LogLevel = "debug", // Enable debug logging + AutoStart = true +}; + +// Create session configuration with specific model +var sessionConfig = new SessionConfig +{ + Model = "gpt-4", + Streaming = false +}; + +// Create an agent with custom configuration +AIAgent agent = new GithubCopilotAgent( + copilotClientOptions, + sessionConfig, + id: "my-copilot-agent", + name: "My Copilot Assistant", + description: "A helpful AI assistant powered by GitHub Copilot" +); + +// Use the agent +AgentResponse response = await agent.RunAsync("What is the weather like today?"); +Console.WriteLine(response); +``` + +## Streaming Responses + +To get streaming responses: + +```csharp +await foreach (var update in agent.RunStreamingAsync("Tell me a story")) +{ + Console.Write(update.Text); +} +``` diff --git a/dotnet/samples/GettingStarted/AgentProviders/README.md b/dotnet/samples/GettingStarted/AgentProviders/README.md index 964e560c9a..4b276215db 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/README.md +++ b/dotnet/samples/GettingStarted/AgentProviders/README.md @@ -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.
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| diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs new file mode 100644 index 0000000000..b4827f0e3a --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -0,0 +1,348 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using GitHub.Copilot.SDK; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI.GithubCopilot; + +/// +/// Represents an that uses the GitHub Copilot SDK to provide agentic capabilities. +/// +public sealed class GithubCopilotAgent : AIAgent, IAsyncDisposable +{ + private readonly CopilotClient _copilotClient; + private readonly string? _id; + private readonly string? _name; + private readonly string? _description; + private readonly SessionConfig? _sessionConfig; + private readonly ILogger _logger; + private readonly bool _ownsClient; + + /// + /// Initializes a new instance of the class. + /// + /// The Copilot client to use for interacting with GitHub Copilot. + /// Optional session configuration for the agent. + /// The unique identifier for the agent. + /// The name of the agent. + /// The description of the agent. + /// Optional logger factory to use for logging. + public GithubCopilotAgent( + CopilotClient copilotClient, + SessionConfig? sessionConfig = null, + string? id = null, + string? name = null, + string? description = null, + ILoggerFactory? loggerFactory = null) + { + _ = Throw.IfNull(copilotClient); + + this._copilotClient = copilotClient; + this._sessionConfig = sessionConfig; + this._id = id; + this._name = name; + this._description = description; + this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger(); + this._ownsClient = false; + } + + /// + /// Initializes a new instance of the class with custom options. + /// + /// Options for creating the Copilot client. + /// Optional session configuration for the agent. + /// The unique identifier for the agent. + /// The name of the agent. + /// The description of the agent. + /// Optional logger factory to use for logging. + public GithubCopilotAgent( + CopilotClientOptions copilotClientOptions, + SessionConfig? sessionConfig = null, + string? id = null, + string? name = null, + string? description = null, + ILoggerFactory? loggerFactory = null) + { + _ = Throw.IfNull(copilotClientOptions); + + this._copilotClient = new CopilotClient(copilotClientOptions); + this._sessionConfig = sessionConfig; + this._id = id; + this._name = name; + this._description = description; + this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger(); + this._ownsClient = true; + } + + /// + public sealed override ValueTask GetNewThreadAsync(CancellationToken cancellationToken = default) + => new(new GithubCopilotAgentThread()); + + /// + /// Get a new instance using an existing session id, to continue that conversation. + /// + /// The session id to continue. + /// A new instance. + public ValueTask GetNewThreadAsync(string sessionId) + => new(new GithubCopilotAgentThread() { SessionId = sessionId }); + + /// + public override ValueTask DeserializeThreadAsync( + JsonElement serializedThread, + JsonSerializerOptions? jsonSerializerOptions = null, + CancellationToken cancellationToken = default) + => new(new GithubCopilotAgentThread(serializedThread, jsonSerializerOptions)); + + /// + protected override async Task RunCoreAsync( + IEnumerable messages, + AgentThread? thread = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(messages); + + // Ensure we have a valid thread + thread ??= await this.GetNewThreadAsync(cancellationToken).ConfigureAwait(false); + if (thread is not GithubCopilotAgentThread typedThread) + { + throw new InvalidOperationException( + $"The provided thread type {thread.GetType()} is not compatible with the agent. Only GitHub Copilot agent created threads are supported."); + } + + // Ensure the client is started + await this.EnsureClientStartedAsync(cancellationToken).ConfigureAwait(false); + + // Create or resume a session + CopilotSession session; + if (typedThread.SessionId is not null) + { + session = await this._copilotClient.ResumeSessionAsync(typedThread.SessionId, cancellationToken: cancellationToken).ConfigureAwait(false); + } + else + { + session = await this._copilotClient.CreateSessionAsync(this._sessionConfig, cancellationToken).ConfigureAwait(false); + typedThread.SessionId = session.SessionId; + } + + try + { + // Prepare to collect response + List responseMessages = []; + TaskCompletionSource completionSource = new(); + + // Subscribe to session events + IDisposable subscription = session.On(evt => + { + switch (evt) + { + case AssistantMessageEvent assistantMessage: + responseMessages.Add(ConvertToChatMessage(assistantMessage)); + break; + + case SessionIdleEvent: + completionSource.TrySetResult(true); + break; + + case SessionErrorEvent errorEvent: + completionSource.TrySetException(new InvalidOperationException( + $"Session error: {errorEvent.Data?.Message ?? "Unknown error"}")); + break; + } + }); + + try + { + // Send the message + string prompt = string.Join("\n", messages.Select(m => m.Text)); + await session.SendAsync(new MessageOptions { Prompt = prompt }, cancellationToken).ConfigureAwait(false); + + // Wait for completion + await completionSource.Task.ConfigureAwait(false); + + return new AgentResponse(responseMessages) + { + AgentId = this.Id, + ResponseId = responseMessages.LastOrDefault()?.MessageId, + }; + } + finally + { + subscription.Dispose(); + } + } + finally + { + await session.DisposeAsync().ConfigureAwait(false); + } + } + + /// + protected override async IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentThread? thread = null, + AgentRunOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(messages); + + // Ensure we have a valid thread + thread ??= await this.GetNewThreadAsync(cancellationToken).ConfigureAwait(false); + if (thread is not GithubCopilotAgentThread typedThread) + { + throw new InvalidOperationException( + $"The provided thread type {thread.GetType()} is not compatible with the agent. Only GitHub Copilot agent created threads 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, + Streaming = true + } + : new SessionConfig { Streaming = true }; + + CopilotSession session; + if (typedThread.SessionId is not null) + { + session = await this._copilotClient.ResumeSessionAsync( + typedThread.SessionId, + new ResumeSessionConfig { Streaming = true }, + cancellationToken).ConfigureAwait(false); + } + else + { + session = await this._copilotClient.CreateSessionAsync(sessionConfig, cancellationToken).ConfigureAwait(false); + typedThread.SessionId = session.SessionId; + } + + try + { + TaskCompletionSource completionSource = new(); + List updates = []; + + // Subscribe to session events + IDisposable subscription = session.On(evt => + { + switch (evt) + { + case AssistantMessageDeltaEvent deltaEvent: + updates.Add(ConvertToAgentResponseUpdate(deltaEvent)); + break; + + case AssistantMessageEvent assistantMessage: + updates.Add(ConvertToAgentResponseUpdate(assistantMessage)); + break; + + case SessionIdleEvent: + completionSource.TrySetResult(true); + break; + + case SessionErrorEvent errorEvent: + completionSource.TrySetException(new InvalidOperationException( + $"Session error: {errorEvent.Data?.Message ?? "Unknown error"}")); + break; + } + }); + + try + { + // Send the message + string prompt = string.Join("\n", messages.Select(m => m.Text)); + await session.SendAsync(new MessageOptions { Prompt = prompt }, cancellationToken).ConfigureAwait(false); + + // Wait for completion + await completionSource.Task.ConfigureAwait(false); + + // Yield all collected updates + foreach (AgentResponseUpdate update in updates) + { + yield return update; + } + } + finally + { + subscription.Dispose(); + } + } + finally + { + await session.DisposeAsync().ConfigureAwait(false); + } + } + + /// + protected override string? IdCore => this._id; + + /// + public override string? Name => this._name; + + /// + public override string? Description => this._description; + + /// + /// Disposes the agent and releases resources. + /// + /// A value task representing the asynchronous dispose operation. + 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 ChatMessage ConvertToChatMessage(AssistantMessageEvent assistantMessage) + { + return new ChatMessage(ChatRole.Assistant, assistantMessage.Data?.Content ?? string.Empty) + { + MessageId = assistantMessage.Data?.MessageId + }; + } + + private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageDeltaEvent deltaEvent) + { + return new AgentResponseUpdate(ChatRole.Assistant, [new TextContent(deltaEvent.Data?.DeltaContent ?? string.Empty)]) + { + AgentId = this.Id, + MessageId = deltaEvent.Data?.MessageId + }; + } + + private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent assistantMessage) + { + return new AgentResponseUpdate(ChatRole.Assistant, [new TextContent(assistantMessage.Data?.Content ?? string.Empty)]) + { + AgentId = this.Id, + ResponseId = assistantMessage.Data?.MessageId, + MessageId = assistantMessage.Data?.MessageId + }; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs new file mode 100644 index 0000000000..1da34a9605 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.GithubCopilot; + +/// +/// Represents a thread for a GitHub Copilot agent conversation. +/// +public sealed class GithubCopilotAgentThread : AgentThread +{ + /// + /// Gets or sets the session ID for the GitHub Copilot conversation. + /// + public string? SessionId { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public GithubCopilotAgentThread() + { + } + + /// + /// Initializes a new instance of the class from serialized data. + /// + /// The serialized thread data. + /// Optional JSON serialization options. + internal GithubCopilotAgentThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) + { + // Try both SessionId (PascalCase) and sessionId (camelCase) for compatibility +#pragma warning disable CA1507 // Use nameof to express symbol names - Need to check both casings for compatibility + if (serializedThread.TryGetProperty("SessionId", out JsonElement sessionIdElement) || + serializedThread.TryGetProperty("sessionId", out sessionIdElement)) +#pragma warning restore CA1507 + { + this.SessionId = sessionIdElement.GetString(); + } + } + + /// + public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) + { + GithubCopilotAgentThreadState state = new() + { + SessionId = this.SessionId + }; + + return JsonSerializer.SerializeToElement( + state, + GithubCopilotJsonUtilities.DefaultOptions.GetTypeInfo(typeof(GithubCopilotAgentThreadState))); + } + + internal sealed class GithubCopilotAgentThreadState + { + public string? SessionId { get; set; } + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs new file mode 100644 index 0000000000..253001c52b --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.GithubCopilot; + +/// +/// Provides utility methods and configurations for JSON serialization operations within the GitHub Copilot agent implementation. +/// +internal static partial class GithubCopilotJsonUtilities +{ + /// + /// Gets the default instance used for JSON serialization operations. + /// + public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions(); + + /// + /// Creates and configures the default JSON serialization options. + /// + /// The configured options. + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")] + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")] + 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!); + + // If reflection-based serialization is enabled by default, include string-based enum serialization. + if (JsonSerializer.IsReflectionEnabledByDefault) + { + options.Converters.Add(new JsonStringEnumConverter()); + } + + options.MakeReadOnly(); + return options; + } + + [JsonSourceGenerationOptions(JsonSerializerDefaults.Web, + UseStringEnumConverter = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + NumberHandling = JsonNumberHandling.AllowReadingFromString)] + [JsonSerializable(typeof(GithubCopilotAgentThread.GithubCopilotAgentThreadState))] + [ExcludeFromCodeCoverage] + private sealed partial class JsonContext : JsonSerializerContext; +} diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/Microsoft.Agents.AI.GithubCopilot.csproj b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/Microsoft.Agents.AI.GithubCopilot.csproj new file mode 100644 index 0000000000..e752bd543e --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/Microsoft.Agents.AI.GithubCopilot.csproj @@ -0,0 +1,34 @@ + + + + preview + + $(TargetFrameworksCore) + + + + true + true + + + + + + + + + + + + + + + Microsoft Agent Framework GitHub Copilot + Provides Microsoft Agent Framework support for GitHub Copilot SDK. + + + + + + + diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs new file mode 100644 index 0000000000..c1f273d38a --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using GitHub.Copilot.SDK; + +namespace Microsoft.Agents.AI.GithubCopilot.UnitTests; + +/// +/// Unit tests for the class. +/// +public sealed class GithubCopilotAgentTests +{ + [Fact] + public void Constructor_WithCopilotClientOptions_InitializesPropertiesCorrectly() + { + // Arrange + var options = new CopilotClientOptions { AutoStart = false }; + const string TestId = "test-id"; + const string TestName = "test-name"; + const string TestDescription = "test-description"; + + // Act + var agent = new GithubCopilotAgent(options, id: TestId, name: TestName, description: TestDescription); + + // Assert + Assert.Equal(TestId, agent.Id); + Assert.Equal(TestName, agent.Name); + Assert.Equal(TestDescription, agent.Description); + } + + [Fact] + public void Constructor_WithNullCopilotClientOptions_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => new GithubCopilotAgent((CopilotClientOptions)null!)); + } + + [Fact] + public void Constructor_WithNullCopilotClient_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => new GithubCopilotAgent((CopilotClient)null!)); + } + + [Fact] + public void Constructor_WithDefaultParameters_UsesBaseProperties() + { + // Arrange + var options = new CopilotClientOptions { AutoStart = false }; + + // Act + var agent = new GithubCopilotAgent(options); + + // Assert + Assert.NotNull(agent.Id); + Assert.NotEmpty(agent.Id); + Assert.Null(agent.Name); + Assert.Null(agent.Description); + } + + [Fact] + public async Task GetNewThreadAsync_ReturnsGithubCopilotAgentThreadAsync() + { + // Arrange + var options = new CopilotClientOptions { AutoStart = false }; + var agent = new GithubCopilotAgent(options); + + // Act + var thread = await agent.GetNewThreadAsync(); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + } + + [Fact] + public async Task GetNewThreadAsync_WithSessionId_ReturnsThreadWithSessionIdAsync() + { + // Arrange + var options = new CopilotClientOptions { AutoStart = false }; + var agent = new GithubCopilotAgent(options); + const string TestSessionId = "test-session-id"; + + // Act + var thread = await agent.GetNewThreadAsync(TestSessionId); + + // Assert + Assert.NotNull(thread); + var typedThread = Assert.IsType(thread); + Assert.Equal(TestSessionId, typedThread.SessionId); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs new file mode 100644 index 0000000000..c1d59d2652 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; + +namespace Microsoft.Agents.AI.GithubCopilot.UnitTests; + +/// +/// Unit tests for the class. +/// +public sealed class GithubCopilotAgentThreadTests +{ + [Fact] + public void Constructor_InitializesWithNullSessionId() + { + // Act + var thread = new GithubCopilotAgentThread(); + + // Assert + Assert.Null(thread.SessionId); + } + + [Fact] + public void SessionId_CanBeSetAndRetrieved() + { + // Arrange + var thread = new GithubCopilotAgentThread(); + const string TestSessionId = "test-session-id"; + + // Act + thread.SessionId = TestSessionId; + + // Assert + Assert.Equal(TestSessionId, thread.SessionId); + } + + [Fact] + public void Constructor_RoundTrip_SerializationPreservesState() + { + // Arrange + const string SessionId = "session-rt-001"; + GithubCopilotAgentThread originalThread = new() { SessionId = SessionId }; + + // Act + JsonElement serialized = originalThread.Serialize(); + + // Debug output + Console.WriteLine($"Serialized JSON: {serialized.GetRawText()}"); + + GithubCopilotAgentThread deserializedThread = new(serialized); + + // Assert + Assert.Equal(originalThread.SessionId, deserializedThread.SessionId); + } + + [Fact] + public void Deserialize_WithSessionId_DeserializesCorrectly() + { + // Arrange + const string Json = """{"SessionId":"test-session-id"}"""; + JsonDocument doc = JsonDocument.Parse(Json); + + // Act + var thread = new GithubCopilotAgentThread(doc.RootElement); + + // Assert + Assert.Equal("test-session-id", thread.SessionId); + } + + [Fact] + public void Deserialize_WithoutSessionId_HasNullSessionId() + { + // Arrange + const string Json = """{}"""; + JsonDocument doc = JsonDocument.Parse(Json); + + // Act + var thread = new GithubCopilotAgentThread(doc.RootElement); + + // Assert + Assert.Null(thread.SessionId); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/Microsoft.Agents.AI.GithubCopilot.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/Microsoft.Agents.AI.GithubCopilot.UnitTests.csproj new file mode 100644 index 0000000000..ef8527302e --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/Microsoft.Agents.AI.GithubCopilot.UnitTests.csproj @@ -0,0 +1,12 @@ + + + + + $(TargetFrameworksCore) + + + + + + + From 91780956d5d485f187f7b0f05b89ac785dc83e14 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:13:17 +0000 Subject: [PATCH 03/34] Add projects to solution and fix sample imports Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> --- dotnet/agent-framework-dotnet.slnx | 3 +++ .../AgentProviders/Agent_With_GithubCopilot/Program.cs | 2 +- .../GithubCopilotAgentThreadTests.cs | 4 ---- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 8b1b00fd2b..4fcb30ae5c 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -65,6 +65,7 @@ + @@ -395,6 +396,7 @@ + @@ -438,6 +440,7 @@ + diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs index 4af23f1875..15183404f6 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs @@ -3,8 +3,8 @@ // This sample shows how to create and use a simple AI agent with GitHub Copilot SDK. using GitHub.Copilot.SDK; +using Microsoft.Agents.AI; using Microsoft.Agents.AI.GithubCopilot; -using Microsoft.Extensions.AI; // Create a Copilot client with default options var copilotClientOptions = new CopilotClientOptions diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs index c1d59d2652..288b75dc4e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs @@ -43,10 +43,6 @@ public void Constructor_RoundTrip_SerializationPreservesState() // Act JsonElement serialized = originalThread.Serialize(); - - // Debug output - Console.WriteLine($"Serialized JSON: {serialized.GetRawText()}"); - GithubCopilotAgentThread deserializedThread = new(serialized); // Assert From b365707718884a7e5122e5d3eaa802418ad331cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:17:41 +0000 Subject: [PATCH 04/34] Improve pragma comment clarity in GithubCopilotAgentThread Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> --- .../GithubCopilotAgentThread.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs index 1da34a9605..7d7e11ec8e 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs @@ -31,7 +31,8 @@ public GithubCopilotAgentThread() internal GithubCopilotAgentThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) { // Try both SessionId (PascalCase) and sessionId (camelCase) for compatibility -#pragma warning disable CA1507 // Use nameof to express symbol names - Need to check both casings for compatibility + // The JSON serialization uses camelCase by default, but we check both for robustness +#pragma warning disable CA1507 // Use nameof to express symbol names - Need literal strings to check both PascalCase and camelCase variants if (serializedThread.TryGetProperty("SessionId", out JsonElement sessionIdElement) || serializedThread.TryGetProperty("sessionId", out sessionIdElement)) #pragma warning restore CA1507 From 44f3ab677a07b7daf7f158ee46042af477d1533a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 09:41:10 +0000 Subject: [PATCH 05/34] 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> --- .../Agent_With_GithubCopilot/Program.cs | 16 ++--- .../Agent_With_GithubCopilot/README.md | 14 ++-- .../GithubCopilotAgent.cs | 64 ++++--------------- .../GithubCopilotAgentThread.cs | 18 ++---- .../GithubCopilotJsonUtilities.cs | 2 +- .../GithubCopilotAgentTests.cs | 27 +++----- .../GithubCopilotAgentThreadTests.cs | 12 ++-- 7 files changed, 52 insertions(+), 101 deletions(-) diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs index 15183404f6..14ff93b8d9 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs @@ -6,15 +6,13 @@ using Microsoft.Agents.AI; using Microsoft.Agents.AI.GithubCopilot; -// Create a Copilot client with default options -var copilotClientOptions = new CopilotClientOptions -{ - AutoStart = true -}; +// Create and start a Copilot client +await using CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = true }); +await copilotClient.StartAsync(); -// Create an instance of the AIAgent using GitHub Copilot SDK -AIAgent agent = new GithubCopilotAgent(copilotClientOptions); +// Create an instance of the AIAgent using the Copilot client +AIAgent agent = new GithubCopilotAgent(copilotClient); -// Invoke the agent and output the text result -AgentResponse response = await agent.RunAsync("Tell me a joke about a pirate."); +// Ask Copilot to write code for us - demonstrate its code generation capabilities +AgentResponse response = await agent.RunAsync("Write a small .NET 10 C# hello world single file application"); Console.WriteLine(response); diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md index 3283873d69..5c859a1cb6 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md @@ -1,5 +1,9 @@ # 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 @@ -33,16 +37,18 @@ You can customize the agent by providing additional configuration: ```csharp using GitHub.Copilot.SDK; +using Microsoft.Agents.AI; using Microsoft.Agents.AI.GithubCopilot; -using Microsoft.Extensions.AI; // Create a Copilot client with custom options -var copilotClientOptions = new CopilotClientOptions +await using CopilotClient copilotClient = new(new CopilotClientOptions { CliPath = "/custom/path/to/copilot", // Custom CLI path LogLevel = "debug", // Enable debug logging AutoStart = true -}; +}); + +await copilotClient.StartAsync(); // Create session configuration with specific model var sessionConfig = new SessionConfig @@ -53,7 +59,7 @@ var sessionConfig = new SessionConfig // Create an agent with custom configuration AIAgent agent = new GithubCopilotAgent( - copilotClientOptions, + copilotClient, sessionConfig, id: "my-copilot-agent", name: "My Copilot Assistant", diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index b4827f0e3a..f34e6d0d25 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -18,7 +18,7 @@ namespace Microsoft.Agents.AI.GithubCopilot; /// /// Represents an that uses the GitHub Copilot SDK to provide agentic capabilities. /// -public sealed class GithubCopilotAgent : AIAgent, IAsyncDisposable +public sealed class GithubCopilotAgent : AIAgent { private readonly CopilotClient _copilotClient; private readonly string? _id; @@ -26,7 +26,6 @@ public sealed class GithubCopilotAgent : AIAgent, IAsyncDisposable private readonly string? _description; private readonly SessionConfig? _sessionConfig; private readonly ILogger _logger; - private readonly bool _ownsClient; /// /// Initializes a new instance of the class. @@ -53,35 +52,6 @@ public GithubCopilotAgent( this._name = name; this._description = description; this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger(); - this._ownsClient = false; - } - - /// - /// Initializes a new instance of the class with custom options. - /// - /// Options for creating the Copilot client. - /// Optional session configuration for the agent. - /// The unique identifier for the agent. - /// The name of the agent. - /// The description of the agent. - /// Optional logger factory to use for logging. - public GithubCopilotAgent( - CopilotClientOptions copilotClientOptions, - SessionConfig? sessionConfig = null, - string? id = null, - string? name = null, - string? description = null, - ILoggerFactory? loggerFactory = null) - { - _ = Throw.IfNull(copilotClientOptions); - - this._copilotClient = new CopilotClient(copilotClientOptions); - this._sessionConfig = sessionConfig; - this._id = id; - this._name = name; - this._description = description; - this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger(); - this._ownsClient = true; } /// @@ -238,7 +208,7 @@ protected override async IAsyncEnumerable RunCoreStreamingA try { TaskCompletionSource completionSource = new(); - List updates = []; + System.Threading.Channels.Channel channel = System.Threading.Channels.Channel.CreateUnbounded(); // Subscribe to session events IDisposable subscription = session.On(evt => @@ -246,20 +216,23 @@ protected override async IAsyncEnumerable RunCoreStreamingA switch (evt) { case AssistantMessageDeltaEvent deltaEvent: - updates.Add(ConvertToAgentResponseUpdate(deltaEvent)); + channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(deltaEvent)); break; case AssistantMessageEvent assistantMessage: - updates.Add(ConvertToAgentResponseUpdate(assistantMessage)); + channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(assistantMessage)); break; case SessionIdleEvent: + channel.Writer.Complete(); completionSource.TrySetResult(true); break; case SessionErrorEvent errorEvent: - completionSource.TrySetException(new InvalidOperationException( - $"Session error: {errorEvent.Data?.Message ?? "Unknown error"}")); + Exception exception = new InvalidOperationException( + $"Session error: {errorEvent.Data?.Message ?? "Unknown error"}"); + channel.Writer.Complete(exception); + completionSource.TrySetException(exception); break; } }); @@ -270,11 +243,8 @@ protected override async IAsyncEnumerable RunCoreStreamingA string prompt = string.Join("\n", messages.Select(m => m.Text)); await session.SendAsync(new MessageOptions { Prompt = prompt }, cancellationToken).ConfigureAwait(false); - // Wait for completion - await completionSource.Task.ConfigureAwait(false); - - // Yield all collected updates - foreach (AgentResponseUpdate update in updates) + // Yield updates as they arrive + await foreach (AgentResponseUpdate update in channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { yield return update; } @@ -299,18 +269,6 @@ protected override async IAsyncEnumerable RunCoreStreamingA /// public override string? Description => this._description; - /// - /// Disposes the agent and releases resources. - /// - /// A value task representing the asynchronous dispose operation. - 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) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs index 7d7e11ec8e..5002671ac0 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs @@ -14,12 +14,12 @@ public sealed class GithubCopilotAgentThread : AgentThread /// /// Gets or sets the session ID for the GitHub Copilot conversation. /// - public string? SessionId { get; set; } + public string? SessionId { get; internal set; } /// /// Initializes a new instance of the class. /// - public GithubCopilotAgentThread() + internal GithubCopilotAgentThread() { } @@ -30,12 +30,8 @@ public GithubCopilotAgentThread() /// Optional JSON serialization options. internal GithubCopilotAgentThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) { - // Try both SessionId (PascalCase) and sessionId (camelCase) for compatibility - // The JSON serialization uses camelCase by default, but we check both for robustness -#pragma warning disable CA1507 // Use nameof to express symbol names - Need literal strings to check both PascalCase and camelCase variants - if (serializedThread.TryGetProperty("SessionId", out JsonElement sessionIdElement) || - serializedThread.TryGetProperty("sessionId", out sessionIdElement)) -#pragma warning restore CA1507 + // The JSON serialization uses camelCase + if (serializedThread.TryGetProperty("sessionId", out JsonElement sessionIdElement)) { this.SessionId = sessionIdElement.GetString(); } @@ -44,17 +40,17 @@ internal GithubCopilotAgentThread(JsonElement serializedThread, JsonSerializerOp /// public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) { - GithubCopilotAgentThreadState state = new() + State state = new() { SessionId = this.SessionId }; return JsonSerializer.SerializeToElement( state, - GithubCopilotJsonUtilities.DefaultOptions.GetTypeInfo(typeof(GithubCopilotAgentThreadState))); + GithubCopilotJsonUtilities.DefaultOptions.GetTypeInfo(typeof(State))); } - internal sealed class GithubCopilotAgentThreadState + internal sealed class State { public string? SessionId { get; set; } } diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs index 253001c52b..ed157380c0 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs @@ -51,7 +51,7 @@ private static JsonSerializerOptions CreateDefaultOptions() UseStringEnumConverter = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, NumberHandling = JsonNumberHandling.AllowReadingFromString)] - [JsonSerializable(typeof(GithubCopilotAgentThread.GithubCopilotAgentThreadState))] + [JsonSerializable(typeof(GithubCopilotAgentThread.State))] [ExcludeFromCodeCoverage] private sealed partial class JsonContext : JsonSerializerContext; } diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs index c1f273d38a..26f69636da 100644 --- a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs @@ -12,16 +12,16 @@ namespace Microsoft.Agents.AI.GithubCopilot.UnitTests; public sealed class GithubCopilotAgentTests { [Fact] - public void Constructor_WithCopilotClientOptions_InitializesPropertiesCorrectly() + public async Task Constructor_WithCopilotClient_InitializesPropertiesCorrectlyAsync() { // Arrange - var options = new CopilotClientOptions { AutoStart = false }; + await using 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(options, id: TestId, name: TestName, description: TestDescription); + var agent = new GithubCopilotAgent(copilotClient, id: TestId, name: TestName, description: TestDescription); // Assert Assert.Equal(TestId, agent.Id); @@ -29,13 +29,6 @@ public void Constructor_WithCopilotClientOptions_InitializesPropertiesCorrectly( Assert.Equal(TestDescription, agent.Description); } - [Fact] - public void Constructor_WithNullCopilotClientOptions_ThrowsArgumentNullException() - { - // Act & Assert - Assert.Throws(() => new GithubCopilotAgent((CopilotClientOptions)null!)); - } - [Fact] public void Constructor_WithNullCopilotClient_ThrowsArgumentNullException() { @@ -44,13 +37,13 @@ public void Constructor_WithNullCopilotClient_ThrowsArgumentNullException() } [Fact] - public void Constructor_WithDefaultParameters_UsesBaseProperties() + public async Task Constructor_WithDefaultParameters_UsesBasePropertiesAsync() { // Arrange - var options = new CopilotClientOptions { AutoStart = false }; + await using CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); // Act - var agent = new GithubCopilotAgent(options); + var agent = new GithubCopilotAgent(copilotClient); // Assert Assert.NotNull(agent.Id); @@ -63,8 +56,8 @@ public void Constructor_WithDefaultParameters_UsesBaseProperties() public async Task GetNewThreadAsync_ReturnsGithubCopilotAgentThreadAsync() { // Arrange - var options = new CopilotClientOptions { AutoStart = false }; - var agent = new GithubCopilotAgent(options); + await using CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); + var agent = new GithubCopilotAgent(copilotClient); // Act var thread = await agent.GetNewThreadAsync(); @@ -78,8 +71,8 @@ public async Task GetNewThreadAsync_ReturnsGithubCopilotAgentThreadAsync() public async Task GetNewThreadAsync_WithSessionId_ReturnsThreadWithSessionIdAsync() { // Arrange - var options = new CopilotClientOptions { AutoStart = false }; - var agent = new GithubCopilotAgent(options); + await using CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); + var agent = new GithubCopilotAgent(copilotClient); const string TestSessionId = "test-session-id"; // Act diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs index 288b75dc4e..a4ed0cb479 100644 --- a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs @@ -21,17 +21,17 @@ public void Constructor_InitializesWithNullSessionId() } [Fact] - public void SessionId_CanBeSetAndRetrieved() + public void SessionId_IsInternalSet() { // Arrange - var thread = new GithubCopilotAgentThread(); - const string TestSessionId = "test-session-id"; + const string Json = """{"sessionId":"test-value"}"""; + JsonDocument doc = JsonDocument.Parse(Json); // Act - thread.SessionId = TestSessionId; + var thread = new GithubCopilotAgentThread(doc.RootElement); // Assert - Assert.Equal(TestSessionId, thread.SessionId); + Assert.Equal("test-value", thread.SessionId); } [Fact] @@ -53,7 +53,7 @@ public void Constructor_RoundTrip_SerializationPreservesState() public void Deserialize_WithSessionId_DeserializesCorrectly() { // Arrange - const string Json = """{"SessionId":"test-session-id"}"""; + const string Json = """{"sessionId":"test-session-id"}"""; JsonDocument doc = JsonDocument.Parse(Json); // Act From 52ba62743e23b151d3ba27a62287eaba68a6bb27 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 10:48:02 +0000 Subject: [PATCH 06/34] Add ownsClient parameter to allow caller control over client disposal Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> --- .../GithubCopilotAgent.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index f34e6d0d25..c1e53dce3b 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -18,7 +18,7 @@ namespace Microsoft.Agents.AI.GithubCopilot; /// /// Represents an that uses the GitHub Copilot SDK to provide agentic capabilities. /// -public sealed class GithubCopilotAgent : AIAgent +public sealed class GithubCopilotAgent : AIAgent, IAsyncDisposable { private readonly CopilotClient _copilotClient; private readonly string? _id; @@ -26,12 +26,14 @@ public sealed class GithubCopilotAgent : AIAgent private readonly string? _description; private readonly SessionConfig? _sessionConfig; private readonly ILogger _logger; + private readonly bool _ownsClient; /// /// Initializes a new instance of the class. /// /// The Copilot client to use for interacting with GitHub Copilot. /// Optional session configuration for the agent. + /// Whether the agent owns the client and should dispose it. Default is false. /// The unique identifier for the agent. /// The name of the agent. /// The description of the agent. @@ -39,6 +41,7 @@ public sealed class GithubCopilotAgent : AIAgent public GithubCopilotAgent( CopilotClient copilotClient, SessionConfig? sessionConfig = null, + bool ownsClient = false, string? id = null, string? name = null, string? description = null, @@ -48,6 +51,7 @@ public GithubCopilotAgent( this._copilotClient = copilotClient; this._sessionConfig = sessionConfig; + this._ownsClient = ownsClient; this._id = id; this._name = name; this._description = description; @@ -269,6 +273,18 @@ protected override async IAsyncEnumerable RunCoreStreamingA /// public override string? Description => this._description; + /// + /// Disposes the agent and releases resources. + /// + /// A value task representing the asynchronous dispose operation. + 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) From c348e7d96fdfa36f2b60c6c7ede0fb91df2f043e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 11:04:56 +0000 Subject: [PATCH 07/34] Fix unit tests by removing await using to avoid StreamJsonRpc disposal issues Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> --- .../GithubCopilotAgentTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs index 26f69636da..6cf29e0eb9 100644 --- a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs @@ -12,10 +12,10 @@ namespace Microsoft.Agents.AI.GithubCopilot.UnitTests; public sealed class GithubCopilotAgentTests { [Fact] - public async Task Constructor_WithCopilotClient_InitializesPropertiesCorrectlyAsync() + public void Constructor_WithCopilotClient_InitializesPropertiesCorrectly() { // Arrange - await using CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); + CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); const string TestId = "test-id"; const string TestName = "test-name"; const string TestDescription = "test-description"; @@ -37,10 +37,10 @@ public void Constructor_WithNullCopilotClient_ThrowsArgumentNullException() } [Fact] - public async Task Constructor_WithDefaultParameters_UsesBasePropertiesAsync() + public void Constructor_WithDefaultParameters_UsesBaseProperties() { // Arrange - await using CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); + CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); // Act var agent = new GithubCopilotAgent(copilotClient); @@ -56,7 +56,7 @@ public async Task Constructor_WithDefaultParameters_UsesBasePropertiesAsync() public async Task GetNewThreadAsync_ReturnsGithubCopilotAgentThreadAsync() { // Arrange - await using CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); + CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); var agent = new GithubCopilotAgent(copilotClient); // Act @@ -71,7 +71,7 @@ public async Task GetNewThreadAsync_ReturnsGithubCopilotAgentThreadAsync() public async Task GetNewThreadAsync_WithSessionId_ReturnsThreadWithSessionIdAsync() { // Arrange - await using CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); + CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); var agent = new GithubCopilotAgent(copilotClient); const string TestSessionId = "test-session-id"; From b45d14a088f82c81d02ebf1f8780b7857fd1d615 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 11:18:46 +0000 Subject: [PATCH 08/34] Fix file encoding: add UTF-8 BOM to Program.cs Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> --- .../AgentProviders/Agent_With_GithubCopilot/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs index 14ff93b8d9..ce6acae1ea 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // This sample shows how to create and use a simple AI agent with GitHub Copilot SDK. From c3f94cb0c0e1dd9a34e3a55269728070490ca647 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 12:25:49 +0000 Subject: [PATCH 09/34] 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> --- .../GithubCopilotAgent.cs | 12 +++--------- .../GithubCopilotAgentThread.cs | 4 +--- .../GithubCopilotJsonUtilities.cs | 3 +-- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index c1e53dce3b..d9bc026bb1 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; @@ -9,8 +9,6 @@ using System.Threading.Tasks; using GitHub.Copilot.SDK; using Microsoft.Extensions.AI; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.GithubCopilot; @@ -25,7 +23,6 @@ public sealed class GithubCopilotAgent : AIAgent, IAsyncDisposable private readonly string? _name; private readonly string? _description; private readonly SessionConfig? _sessionConfig; - private readonly ILogger _logger; private readonly bool _ownsClient; /// @@ -37,15 +34,13 @@ public sealed class GithubCopilotAgent : AIAgent, IAsyncDisposable /// The unique identifier for the agent. /// The name of the agent. /// The description of the agent. - /// Optional logger factory to use for logging. public GithubCopilotAgent( CopilotClient copilotClient, SessionConfig? sessionConfig = null, bool ownsClient = false, string? id = null, string? name = null, - string? description = null, - ILoggerFactory? loggerFactory = null) + string? description = null) { _ = Throw.IfNull(copilotClient); @@ -55,7 +50,6 @@ public GithubCopilotAgent( this._id = id; this._name = name; this._description = description; - this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger(); } /// @@ -121,7 +115,7 @@ protected override async Task RunCoreAsync( switch (evt) { case AssistantMessageEvent assistantMessage: - responseMessages.Add(ConvertToChatMessage(assistantMessage)); + responseMessages.Add(this.ConvertToChatMessage(assistantMessage)); break; case SessionIdleEvent: diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs index 5002671ac0..5e31d25227 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs @@ -1,8 +1,6 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. -using System; using System.Text.Json; -using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.GithubCopilot; diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs index ed157380c0..7e78388e5e 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs @@ -1,10 +1,9 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; -using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.GithubCopilot; From 97f4457c131a1271d9f14c99ddb5a6f677b67921 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 12:34:36 +0000 Subject: [PATCH 10/34] Fix test file encoding and remove redundant cast Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> --- .../GithubCopilotAgentTests.cs | 4 ++-- .../GithubCopilotAgentThreadTests.cs | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs index 6cf29e0eb9..5b974201e6 100644 --- a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System; using System.Threading.Tasks; @@ -33,7 +33,7 @@ public void Constructor_WithCopilotClient_InitializesPropertiesCorrectly() public void Constructor_WithNullCopilotClient_ThrowsArgumentNullException() { // Act & Assert - Assert.Throws(() => new GithubCopilotAgent((CopilotClient)null!)); + Assert.Throws(() => new GithubCopilotAgent(null!)); } [Fact] diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs index a4ed0cb479..7101d3a0b6 100644 --- a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs @@ -1,6 +1,5 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. -using System; using System.Text.Json; namespace Microsoft.Agents.AI.GithubCopilot.UnitTests; From a5b7aeb728591678c5d70d3aa3edbf11847a2511 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:00:32 +0000 Subject: [PATCH 11/34] Add AsAIAgent extension methods for CopilotClient with tests Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> --- .../Agent_With_GithubCopilot/Program.cs | 5 +- .../Agent_With_GithubCopilot/README.md | 7 +- .../CopilotClientExtensions.cs | 47 ++++++++++++ .../CopilotClientExtensionsTests.cs | 71 +++++++++++++++++++ 4 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/CopilotClientExtensionsTests.cs diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs index ce6acae1ea..4a6c5d2f08 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs @@ -4,14 +4,13 @@ using GitHub.Copilot.SDK; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.GithubCopilot; // Create and start a Copilot client await using CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = true }); await copilotClient.StartAsync(); -// Create an instance of the AIAgent using the Copilot client -AIAgent agent = new GithubCopilotAgent(copilotClient); +// Create an instance of the AIAgent using the extension method +AIAgent agent = copilotClient.AsAIAgent(ownsClient: true); // Ask Copilot to write code for us - demonstrate its code generation capabilities AgentResponse response = await agent.RunAsync("Write a small .NET 10 C# hello world single file application"); diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md index 5c859a1cb6..56610577b5 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md @@ -38,7 +38,6 @@ You can customize the agent by providing additional configuration: ```csharp using GitHub.Copilot.SDK; using Microsoft.Agents.AI; -using Microsoft.Agents.AI.GithubCopilot; // Create a Copilot client with custom options await using CopilotClient copilotClient = new(new CopilotClientOptions @@ -57,10 +56,10 @@ var sessionConfig = new SessionConfig Streaming = false }; -// Create an agent with custom configuration -AIAgent agent = new GithubCopilotAgent( - copilotClient, +// 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" diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs new file mode 100644 index 0000000000..cbed5a0db4 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. + +using GitHub.Copilot.SDK; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.GithubCopilot; +using Microsoft.Shared.Diagnostics; + +namespace GitHub.Copilot.SDK; + +/// +/// Provides extension methods for +/// to simplify the creation of GitHub Copilot agents. +/// +/// +/// These extensions bridge the gap between GitHub Copilot SDK client objects +/// and the Microsoft Agent Framework. +/// +/// They allow developers to easily create AI agents that can interact +/// with GitHub Copilot by handling the conversion from Copilot clients to +/// instances that implement the interface. +/// +/// +public static class CopilotClientExtensions +{ + /// + /// Retrieves an instance of for a GitHub Copilot client. + /// + /// The to use for the agent. + /// Optional session configuration for the agent. + /// Whether the agent owns the client and should dispose it. Default is false. + /// The unique identifier for the agent. + /// The name of the agent. + /// The description of the agent. + /// An instance backed by the GitHub Copilot client. + 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); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/CopilotClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/CopilotClientExtensionsTests.cs new file mode 100644 index 0000000000..75d2fac5b4 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/CopilotClientExtensionsTests.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using GitHub.Copilot.SDK; + +namespace Microsoft.Agents.AI.GithubCopilot.UnitTests; + +/// +/// Unit tests for the class. +/// +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(id: TestId, name: TestName, description: TestDescription); + + // Assert + Assert.NotNull(agent); + Assert.IsType(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(); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + } + + [Fact] + public void AsAIAgent_WithNullClient_ThrowsArgumentNullException() + { + // Arrange + CopilotClient? copilotClient = null; + + // Act & Assert + Assert.Throws(() => copilotClient!.AsAIAgent()); + } + + [Fact] + public void AsAIAgent_WithOwnsClient_ReturnsAgentThatOwnsClient() + { + // Arrange + CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); + + // Act + var agent = copilotClient.AsAIAgent(ownsClient: true); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + } +} From c643abcb42dcaeb482df0c39469f0e434ee74fad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:00:47 +0000 Subject: [PATCH 12/34] Remove IL suppressions, use TryComplete for channel writer, remove TCS from streaming Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../GithubCopilotAgent.cs | 7 ++----- .../GithubCopilotJsonUtilities.cs | 8 -------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index d9bc026bb1..c57b627087 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -205,7 +205,6 @@ protected override async IAsyncEnumerable RunCoreStreamingA try { - TaskCompletionSource completionSource = new(); System.Threading.Channels.Channel channel = System.Threading.Channels.Channel.CreateUnbounded(); // Subscribe to session events @@ -222,15 +221,13 @@ protected override async IAsyncEnumerable RunCoreStreamingA break; case SessionIdleEvent: - channel.Writer.Complete(); - completionSource.TrySetResult(true); + channel.Writer.TryComplete(); break; case SessionErrorEvent errorEvent: Exception exception = new InvalidOperationException( $"Session error: {errorEvent.Data?.Message ?? "Unknown error"}"); - channel.Writer.Complete(exception); - completionSource.TrySetException(exception); + channel.Writer.TryComplete(exception); break; } }); diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs index 7e78388e5e..14f6288d64 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs @@ -21,8 +21,6 @@ internal static partial class GithubCopilotJsonUtilities /// Creates and configures the default JSON serialization options. /// /// The configured options. - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")] private static JsonSerializerOptions CreateDefaultOptions() { // Copy the configuration from the source generated context. @@ -36,12 +34,6 @@ private static JsonSerializerOptions CreateDefaultOptions() options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!); options.TypeInfoResolverChain.Add(JsonContext.Default.Options.TypeInfoResolver!); - // If reflection-based serialization is enabled by default, include string-based enum serialization. - if (JsonSerializer.IsReflectionEnabledByDefault) - { - options.Converters.Add(new JsonStringEnumConverter()); - } - options.MakeReadOnly(); return options; } From 7f82795a218a4ec5c88327ddd5826528129df870 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:19:56 +0000 Subject: [PATCH 13/34] Keep session alive across calls, add tools overload, add tests Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../CopilotClientExtensions.cs | 25 ++ .../GithubCopilotAgent.cs | 221 ++++++++++-------- .../GithubCopilotAgentThread.cs | 23 +- .../CopilotClientExtensionsTests.cs | 17 ++ .../GithubCopilotAgentTests.cs | 17 ++ 5 files changed, 204 insertions(+), 99 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs index cbed5a0db4..ca752cd877 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; using GitHub.Copilot.SDK; using Microsoft.Agents.AI; using Microsoft.Agents.AI.GithubCopilot; +using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; namespace GitHub.Copilot.SDK; @@ -44,4 +46,27 @@ public static AIAgent AsAIAgent( return new GithubCopilotAgent(client, sessionConfig, ownsClient, id, name, description); } + + /// + /// Retrieves an instance of for a GitHub Copilot client with tools. + /// + /// The to use for the agent. + /// The tools to make available to the agent. + /// Whether the agent owns the client and should dispose it. Default is false. + /// The unique identifier for the agent. + /// The name of the agent. + /// The description of the agent. + /// An instance backed by the GitHub Copilot client. + public static AIAgent AsAIAgent( + this CopilotClient client, + IList? tools, + bool ownsClient = false, + string? id = null, + string? name = null, + string? description = null) + { + Throw.IfNull(client); + + return new GithubCopilotAgent(client, tools, ownsClient, id, name, description); + } } diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index c57b627087..49bd7e99c6 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -52,6 +52,32 @@ public GithubCopilotAgent( this._description = description; } + /// + /// Initializes a new instance of the class with tools. + /// + /// The Copilot client to use for interacting with GitHub Copilot. + /// The tools to make available to the agent. + /// Whether the agent owns the client and should dispose it. Default is false. + /// The unique identifier for the agent. + /// The name of the agent. + /// The description of the agent. + public GithubCopilotAgent( + CopilotClient copilotClient, + IList? tools, + bool ownsClient = false, + string? id = null, + string? name = null, + string? description = null) + : this( + copilotClient, + tools is { Count: > 0 } ? new SessionConfig { Tools = tools.OfType().ToList() } : null, + ownsClient, + id, + name, + description) + { + } + /// public sealed override ValueTask GetNewThreadAsync(CancellationToken cancellationToken = default) => new(new GithubCopilotAgentThread()); @@ -91,67 +117,51 @@ protected override async Task RunCoreAsync( // Ensure the client is started await this.EnsureClientStartedAsync(cancellationToken).ConfigureAwait(false); - // Create or resume a session - CopilotSession session; - if (typedThread.SessionId is not null) - { - session = await this._copilotClient.ResumeSessionAsync(typedThread.SessionId, cancellationToken: cancellationToken).ConfigureAwait(false); - } - else - { - session = await this._copilotClient.CreateSessionAsync(this._sessionConfig, cancellationToken).ConfigureAwait(false); - typedThread.SessionId = session.SessionId; - } + // Get or create session + CopilotSession session = await this.GetOrCreateSessionAsync(typedThread, cancellationToken).ConfigureAwait(false); - try - { - // Prepare to collect response - List responseMessages = []; - TaskCompletionSource completionSource = new(); + // Prepare to collect response + List responseMessages = []; + TaskCompletionSource completionSource = new(); - // Subscribe to session events - IDisposable subscription = session.On(evt => + // Subscribe to session events + IDisposable subscription = session.On(evt => + { + switch (evt) { - switch (evt) - { - case AssistantMessageEvent assistantMessage: - responseMessages.Add(this.ConvertToChatMessage(assistantMessage)); - break; + case AssistantMessageEvent assistantMessage: + responseMessages.Add(this.ConvertToChatMessage(assistantMessage)); + break; + + case SessionIdleEvent: + completionSource.TrySetResult(true); + break; + + case SessionErrorEvent errorEvent: + completionSource.TrySetException(new InvalidOperationException( + $"Session error: {errorEvent.Data?.Message ?? "Unknown error"}")); + break; + } + }); - case SessionIdleEvent: - completionSource.TrySetResult(true); - break; + try + { + // Send the message + string prompt = string.Join("\n", messages.Select(m => m.Text)); + await session.SendAsync(new MessageOptions { Prompt = prompt }, cancellationToken).ConfigureAwait(false); - case SessionErrorEvent errorEvent: - completionSource.TrySetException(new InvalidOperationException( - $"Session error: {errorEvent.Data?.Message ?? "Unknown error"}")); - break; - } - }); + // Wait for completion + await completionSource.Task.ConfigureAwait(false); - try + return new AgentResponse(responseMessages) { - // Send the message - string prompt = string.Join("\n", messages.Select(m => m.Text)); - await session.SendAsync(new MessageOptions { Prompt = prompt }, cancellationToken).ConfigureAwait(false); - - // Wait for completion - await completionSource.Task.ConfigureAwait(false); - - return new AgentResponse(responseMessages) - { - AgentId = this.Id, - ResponseId = responseMessages.LastOrDefault()?.MessageId, - }; - } - finally - { - subscription.Dispose(); - } + AgentId = this.Id, + ResponseId = responseMessages.LastOrDefault()?.MessageId, + }; } finally { - await session.DisposeAsync().ConfigureAwait(false); + subscription.Dispose(); } } @@ -175,54 +185,27 @@ protected override async IAsyncEnumerable RunCoreStreamingA // 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, - Streaming = true - } - : new SessionConfig { Streaming = true }; + // Get or create session with streaming enabled + CopilotSession session = await this.GetOrCreateSessionAsync(typedThread, cancellationToken, streaming: true).ConfigureAwait(false); - CopilotSession session; - if (typedThread.SessionId is not null) - { - session = await this._copilotClient.ResumeSessionAsync( - typedThread.SessionId, - new ResumeSessionConfig { Streaming = true }, - cancellationToken).ConfigureAwait(false); - } - else - { - session = await this._copilotClient.CreateSessionAsync(sessionConfig, cancellationToken).ConfigureAwait(false); - typedThread.SessionId = session.SessionId; - } + System.Threading.Channels.Channel channel = System.Threading.Channels.Channel.CreateUnbounded(); - try + // Subscribe to session events + IDisposable subscription = session.On(evt => { - System.Threading.Channels.Channel channel = System.Threading.Channels.Channel.CreateUnbounded(); - - // Subscribe to session events - IDisposable subscription = session.On(evt => + switch (evt) { - switch (evt) - { - case AssistantMessageDeltaEvent deltaEvent: - channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(deltaEvent)); - break; + case AssistantMessageDeltaEvent deltaEvent: + channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(deltaEvent)); + break; - case AssistantMessageEvent assistantMessage: - channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(assistantMessage)); - break; + case AssistantMessageEvent assistantMessage: + channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(assistantMessage)); + break; - case SessionIdleEvent: - channel.Writer.TryComplete(); - break; + case SessionIdleEvent: + channel.Writer.TryComplete(); + break; case SessionErrorEvent errorEvent: Exception exception = new InvalidOperationException( @@ -248,11 +231,6 @@ protected override async IAsyncEnumerable RunCoreStreamingA { subscription.Dispose(); } - } - finally - { - await session.DisposeAsync().ConfigureAwait(false); - } } /// @@ -284,6 +262,53 @@ private async Task EnsureClientStartedAsync(CancellationToken cancellationToken) } } + private async Task GetOrCreateSessionAsync( + GithubCopilotAgentThread thread, + CancellationToken cancellationToken, + bool streaming = false) + { + // If thread already has an active session, reuse it + if (thread.Session is not null) + { + return thread.Session; + } + + // Create or resume session + CopilotSession session; + if (thread.SessionId is not null) + { + // Resume existing session + ResumeSessionConfig resumeConfig = new() { Streaming = streaming }; + session = await this._copilotClient.ResumeSessionAsync( + thread.SessionId, + resumeConfig, + cancellationToken).ConfigureAwait(false); + } + else + { + // Create new session + 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, + Streaming = streaming + } + : new SessionConfig { Streaming = streaming }; + + session = await this._copilotClient.CreateSessionAsync(sessionConfig, cancellationToken).ConfigureAwait(false); + thread.SessionId = session.SessionId; + } + + // Store session in thread + thread.Session = session; + return session; + } + private ChatMessage ConvertToChatMessage(AssistantMessageEvent assistantMessage) { return new ChatMessage(ChatRole.Assistant, assistantMessage.Data?.Content ?? string.Empty) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs index 5e31d25227..0df837fb0f 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs @@ -1,19 +1,27 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Text.Json; +using System.Threading.Tasks; +using GitHub.Copilot.SDK; namespace Microsoft.Agents.AI.GithubCopilot; /// /// Represents a thread for a GitHub Copilot agent conversation. /// -public sealed class GithubCopilotAgentThread : AgentThread +public sealed class GithubCopilotAgentThread : AgentThread, IAsyncDisposable { /// /// Gets or sets the session ID for the GitHub Copilot conversation. /// public string? SessionId { get; internal set; } + /// + /// Gets or sets the active Copilot session. + /// + internal CopilotSession? Session { get; set; } + /// /// Initializes a new instance of the class. /// @@ -48,6 +56,19 @@ public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptio GithubCopilotJsonUtilities.DefaultOptions.GetTypeInfo(typeof(State))); } + /// + /// Disposes the thread and releases the session. + /// + /// A value task representing the asynchronous dispose operation. + public async ValueTask DisposeAsync() + { + if (this.Session is not null) + { + await this.Session.DisposeAsync().ConfigureAwait(false); + this.Session = null; + } + } + internal sealed class State { public string? SessionId { get; set; } diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/CopilotClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/CopilotClientExtensionsTests.cs index 75d2fac5b4..662c839866 100644 --- a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/CopilotClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/CopilotClientExtensionsTests.cs @@ -1,7 +1,9 @@ // 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; @@ -68,4 +70,19 @@ public void AsAIAgent_WithOwnsClient_ReturnsAgentThatOwnsClient() Assert.NotNull(agent); Assert.IsType(agent); } + + [Fact] + public void AsAIAgent_WithTools_ReturnsAgentWithTools() + { + // Arrange + CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); + List tools = [AIFunctionFactory.Create(() => "test", "TestFunc", "Test function")]; + + // Act + var agent = copilotClient.AsAIAgent(tools); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs index 5b974201e6..bee14759cd 100644 --- a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs @@ -1,8 +1,10 @@ // 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; @@ -83,4 +85,19 @@ public async Task GetNewThreadAsync_WithSessionId_ReturnsThreadWithSessionIdAsyn var typedThread = Assert.IsType(thread); Assert.Equal(TestSessionId, typedThread.SessionId); } + + [Fact] + public void Constructor_WithTools_InitializesCorrectly() + { + // Arrange + CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); + List tools = [AIFunctionFactory.Create(() => "test", "TestFunc", "Test function")]; + + // Act + var agent = new GithubCopilotAgent(copilotClient, tools); + + // Assert + Assert.NotNull(agent); + Assert.NotNull(agent.Id); + } } From 4d781b815c8d262ce2d6013bed15f32751142e37 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:35:31 +0000 Subject: [PATCH 14/34] Revert session persistence changes - sessions dispose after each call Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../GithubCopilotAgent.cs | 205 +++++++++--------- .../GithubCopilotAgentThread.cs | 23 +- 2 files changed, 104 insertions(+), 124 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index 49bd7e99c6..cc57a3fa09 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -117,51 +117,67 @@ protected override async Task RunCoreAsync( // Ensure the client is started await this.EnsureClientStartedAsync(cancellationToken).ConfigureAwait(false); - // Get or create session - CopilotSession session = await this.GetOrCreateSessionAsync(typedThread, cancellationToken).ConfigureAwait(false); - - // Prepare to collect response - List responseMessages = []; - TaskCompletionSource completionSource = new(); - - // Subscribe to session events - IDisposable subscription = session.On(evt => + // Create or resume a session + CopilotSession session; + if (typedThread.SessionId is not null) { - switch (evt) - { - case AssistantMessageEvent assistantMessage: - responseMessages.Add(this.ConvertToChatMessage(assistantMessage)); - break; - - case SessionIdleEvent: - completionSource.TrySetResult(true); - break; - - case SessionErrorEvent errorEvent: - completionSource.TrySetException(new InvalidOperationException( - $"Session error: {errorEvent.Data?.Message ?? "Unknown error"}")); - break; - } - }); + session = await this._copilotClient.ResumeSessionAsync(typedThread.SessionId, cancellationToken: cancellationToken).ConfigureAwait(false); + } + else + { + session = await this._copilotClient.CreateSessionAsync(this._sessionConfig, cancellationToken).ConfigureAwait(false); + typedThread.SessionId = session.SessionId; + } try { - // Send the message - string prompt = string.Join("\n", messages.Select(m => m.Text)); - await session.SendAsync(new MessageOptions { Prompt = prompt }, cancellationToken).ConfigureAwait(false); + // Prepare to collect response + List responseMessages = []; + TaskCompletionSource completionSource = new(); - // Wait for completion - await completionSource.Task.ConfigureAwait(false); + // Subscribe to session events + IDisposable subscription = session.On(evt => + { + switch (evt) + { + case AssistantMessageEvent assistantMessage: + responseMessages.Add(this.ConvertToChatMessage(assistantMessage)); + break; + + case SessionIdleEvent: + completionSource.TrySetResult(true); + break; + + case SessionErrorEvent errorEvent: + completionSource.TrySetException(new InvalidOperationException( + $"Session error: {errorEvent.Data?.Message ?? "Unknown error"}")); + break; + } + }); - return new AgentResponse(responseMessages) + try + { + // Send the message + string prompt = string.Join("\n", messages.Select(m => m.Text)); + await session.SendAsync(new MessageOptions { Prompt = prompt }, cancellationToken).ConfigureAwait(false); + + // Wait for completion + await completionSource.Task.ConfigureAwait(false); + + return new AgentResponse(responseMessages) + { + AgentId = this.Id, + ResponseId = responseMessages.LastOrDefault()?.MessageId, + }; + } + finally { - AgentId = this.Id, - ResponseId = responseMessages.LastOrDefault()?.MessageId, - }; + subscription.Dispose(); + } } finally { - subscription.Dispose(); + await session.DisposeAsync().ConfigureAwait(false); } } @@ -185,33 +201,60 @@ protected override async IAsyncEnumerable RunCoreStreamingA // Ensure the client is started await this.EnsureClientStartedAsync(cancellationToken).ConfigureAwait(false); - // Get or create session with streaming enabled - CopilotSession session = await this.GetOrCreateSessionAsync(typedThread, cancellationToken, streaming: true).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, + Streaming = true + } + : new SessionConfig { Streaming = true }; - System.Threading.Channels.Channel channel = System.Threading.Channels.Channel.CreateUnbounded(); + CopilotSession session; + if (typedThread.SessionId is not null) + { + session = await this._copilotClient.ResumeSessionAsync( + typedThread.SessionId, + new ResumeSessionConfig { Streaming = true }, + cancellationToken).ConfigureAwait(false); + } + else + { + session = await this._copilotClient.CreateSessionAsync(sessionConfig, cancellationToken).ConfigureAwait(false); + typedThread.SessionId = session.SessionId; + } - // Subscribe to session events - IDisposable subscription = session.On(evt => + try { - switch (evt) - { - case AssistantMessageDeltaEvent deltaEvent: - channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(deltaEvent)); - break; + System.Threading.Channels.Channel channel = System.Threading.Channels.Channel.CreateUnbounded(); - case AssistantMessageEvent assistantMessage: - channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(assistantMessage)); - break; + // Subscribe to session events + IDisposable subscription = session.On(evt => + { + switch (evt) + { + case AssistantMessageDeltaEvent deltaEvent: + channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(deltaEvent)); + break; - case SessionIdleEvent: - channel.Writer.TryComplete(); - break; + case AssistantMessageEvent assistantMessage: + channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(assistantMessage)); + break; - case SessionErrorEvent errorEvent: - Exception exception = new InvalidOperationException( - $"Session error: {errorEvent.Data?.Message ?? "Unknown error"}"); - channel.Writer.TryComplete(exception); + case SessionIdleEvent: + channel.Writer.TryComplete(); break; + + case SessionErrorEvent errorEvent: + Exception exception = new InvalidOperationException( + $"Session error: {errorEvent.Data?.Message ?? "Unknown error"}"); + channel.Writer.TryComplete(exception); + break; } }); @@ -231,6 +274,11 @@ protected override async IAsyncEnumerable RunCoreStreamingA { subscription.Dispose(); } + } + finally + { + await session.DisposeAsync().ConfigureAwait(false); + } } /// @@ -262,53 +310,6 @@ private async Task EnsureClientStartedAsync(CancellationToken cancellationToken) } } - private async Task GetOrCreateSessionAsync( - GithubCopilotAgentThread thread, - CancellationToken cancellationToken, - bool streaming = false) - { - // If thread already has an active session, reuse it - if (thread.Session is not null) - { - return thread.Session; - } - - // Create or resume session - CopilotSession session; - if (thread.SessionId is not null) - { - // Resume existing session - ResumeSessionConfig resumeConfig = new() { Streaming = streaming }; - session = await this._copilotClient.ResumeSessionAsync( - thread.SessionId, - resumeConfig, - cancellationToken).ConfigureAwait(false); - } - else - { - // Create new session - 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, - Streaming = streaming - } - : new SessionConfig { Streaming = streaming }; - - session = await this._copilotClient.CreateSessionAsync(sessionConfig, cancellationToken).ConfigureAwait(false); - thread.SessionId = session.SessionId; - } - - // Store session in thread - thread.Session = session; - return session; - } - private ChatMessage ConvertToChatMessage(AssistantMessageEvent assistantMessage) { return new ChatMessage(ChatRole.Assistant, assistantMessage.Data?.Content ?? string.Empty) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs index 0df837fb0f..5e31d25227 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs @@ -1,27 +1,19 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Text.Json; -using System.Threading.Tasks; -using GitHub.Copilot.SDK; namespace Microsoft.Agents.AI.GithubCopilot; /// /// Represents a thread for a GitHub Copilot agent conversation. /// -public sealed class GithubCopilotAgentThread : AgentThread, IAsyncDisposable +public sealed class GithubCopilotAgentThread : AgentThread { /// /// Gets or sets the session ID for the GitHub Copilot conversation. /// public string? SessionId { get; internal set; } - /// - /// Gets or sets the active Copilot session. - /// - internal CopilotSession? Session { get; set; } - /// /// Initializes a new instance of the class. /// @@ -56,19 +48,6 @@ public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptio GithubCopilotJsonUtilities.DefaultOptions.GetTypeInfo(typeof(State))); } - /// - /// Disposes the thread and releases the session. - /// - /// A value task representing the asynchronous dispose operation. - public async ValueTask DisposeAsync() - { - if (this.Session is not null) - { - await this.Session.DisposeAsync().ConfigureAwait(false); - this.Session = null; - } - } - internal sealed class State { public string? SessionId { get; set; } From 8501a5d22c2dc23684d00806140b49dbd47a3cb1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:42:42 +0000 Subject: [PATCH 15/34] Add CreatedAt property mapping using DateTimeOffset.UtcNow Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../GithubCopilotAgent.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index cc57a3fa09..8cd37e426b 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text.Json; @@ -314,7 +315,8 @@ private ChatMessage ConvertToChatMessage(AssistantMessageEvent assistantMessage) { return new ChatMessage(ChatRole.Assistant, assistantMessage.Data?.Content ?? string.Empty) { - MessageId = assistantMessage.Data?.MessageId + MessageId = assistantMessage.Data?.MessageId, + CreatedAt = DateTimeOffset.UtcNow }; } @@ -323,7 +325,8 @@ private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageDeltaEv return new AgentResponseUpdate(ChatRole.Assistant, [new TextContent(deltaEvent.Data?.DeltaContent ?? string.Empty)]) { AgentId = this.Id, - MessageId = deltaEvent.Data?.MessageId + MessageId = deltaEvent.Data?.MessageId, + CreatedAt = DateTimeOffset.UtcNow }; } @@ -333,7 +336,8 @@ private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent a { AgentId = this.Id, ResponseId = assistantMessage.Data?.MessageId, - MessageId = assistantMessage.Data?.MessageId + MessageId = assistantMessage.Data?.MessageId, + CreatedAt = DateTimeOffset.UtcNow }; } } From a8dd82804a1cdf2cbc8e00f792e08b5c6fae252f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:21:10 +0000 Subject: [PATCH 16/34] Add DataContent handling via temp files and attachments Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../GithubCopilotAgent.cs | 127 +++++++++++++++++- 1 file changed, 123 insertions(+), 4 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index 8cd37e426b..4ba6c77d18 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -156,11 +156,45 @@ protected override async Task RunCoreAsync( } }); + List tempFiles = []; try { - // Send the message + // Build prompt from text content string prompt = string.Join("\n", messages.Select(m => m.Text)); - await session.SendAsync(new MessageOptions { Prompt = prompt }, cancellationToken).ConfigureAwait(false); + + // Handle DataContent as attachments + List? 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(), $"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 = System.IO.Path.GetFileName(tempFilePath) + }); + } + } + } + + // Send the message with attachments + MessageOptions messageOptions = new() { Prompt = prompt }; + if (attachments is not null) + { + messageOptions.Attachments = [.. attachments]; + } + + await session.SendAsync(messageOptions, cancellationToken).ConfigureAwait(false); // Wait for completion await completionSource.Task.ConfigureAwait(false); @@ -174,6 +208,22 @@ protected override async Task RunCoreAsync( finally { subscription.Dispose(); + + // Clean up temp files + foreach (string tempFile in tempFiles) + { + try + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + catch + { + // Best effort cleanup + } + } } } finally @@ -259,11 +309,45 @@ protected override async IAsyncEnumerable RunCoreStreamingA } }); + List tempFiles = []; try { - // Send the message + // Build prompt from text content string prompt = string.Join("\n", messages.Select(m => m.Text)); - await session.SendAsync(new MessageOptions { Prompt = prompt }, cancellationToken).ConfigureAwait(false); + + // Handle DataContent as attachments + List? 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(), $"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 = System.IO.Path.GetFileName(tempFilePath) + }); + } + } + } + + // Send the message with attachments + MessageOptions messageOptions = new() { Prompt = prompt }; + if (attachments is not null) + { + messageOptions.Attachments = [.. attachments]; + } + + await session.SendAsync(messageOptions, cancellationToken).ConfigureAwait(false); // Yield updates as they arrive await foreach (AgentResponseUpdate update in channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) @@ -274,6 +358,22 @@ protected override async IAsyncEnumerable RunCoreStreamingA finally { subscription.Dispose(); + + // Clean up temp files + foreach (string tempFile in tempFiles) + { + try + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + catch + { + // Best effort cleanup + } + } } } finally @@ -340,4 +440,23 @@ private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent a CreatedAt = DateTimeOffset.UtcNow }; } + + private static string GetExtensionForMediaType(string? mediaType) + { + return (mediaType?.ToUpperInvariant()) switch + { + "IMAGE/PNG" => ".png", + "IMAGE/JPEG" or "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", + _ => ".dat" + }; + } } From e28bb5e5b9bea6a04d50a3ef835a95daf2108d38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:24:01 +0000 Subject: [PATCH 17/34] Fix formatting: remove extra indentation, simplify Path references, remove unused using Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../CopilotClientExtensions.cs | 1 - .../GithubCopilotAgent.cs | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs index ca752cd877..e7cd75baef 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using GitHub.Copilot.SDK; using Microsoft.Agents.AI; using Microsoft.Agents.AI.GithubCopilot; using Microsoft.Extensions.AI; diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index 4ba6c77d18..4f83923bf7 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -181,7 +181,7 @@ protected override async Task RunCoreAsync( { Type = UserMessageDataAttachmentsItemType.File, Path = tempFilePath, - DisplayName = System.IO.Path.GetFileName(tempFilePath) + DisplayName = Path.GetFileName(tempFilePath) }); } } @@ -301,11 +301,11 @@ protected override async IAsyncEnumerable RunCoreStreamingA channel.Writer.TryComplete(); break; - case SessionErrorEvent errorEvent: - Exception exception = new InvalidOperationException( - $"Session error: {errorEvent.Data?.Message ?? "Unknown error"}"); - channel.Writer.TryComplete(exception); - break; + case SessionErrorEvent errorEvent: + Exception exception = new InvalidOperationException( + $"Session error: {errorEvent.Data?.Message ?? "Unknown error"}"); + channel.Writer.TryComplete(exception); + break; } }); @@ -334,7 +334,7 @@ protected override async IAsyncEnumerable RunCoreStreamingA { Type = UserMessageDataAttachmentsItemType.File, Path = tempFilePath, - DisplayName = System.IO.Path.GetFileName(tempFilePath) + DisplayName = Path.GetFileName(tempFilePath) }); } } From 6836ca14a974da3d9942506c077c4588a66703d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:27:02 +0000 Subject: [PATCH 18/34] Refactor: extract helper methods to reduce duplication in DataContent handling Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../GithubCopilotAgent.cs | 138 ++++++++---------- 1 file changed, 60 insertions(+), 78 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index 4f83923bf7..78a4d2383a 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -163,29 +163,10 @@ protected override async Task RunCoreAsync( string prompt = string.Join("\n", messages.Select(m => m.Text)); // Handle DataContent as attachments - List? 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(), $"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) - }); - } - } - } + List? attachments = await ProcessDataContentAttachmentsAsync( + messages, + tempFiles, + cancellationToken).ConfigureAwait(false); // Send the message with attachments MessageOptions messageOptions = new() { Prompt = prompt }; @@ -208,22 +189,7 @@ protected override async Task RunCoreAsync( finally { subscription.Dispose(); - - // Clean up temp files - foreach (string tempFile in tempFiles) - { - try - { - if (File.Exists(tempFile)) - { - File.Delete(tempFile); - } - } - catch - { - // Best effort cleanup - } - } + CleanupTempFiles(tempFiles); } } finally @@ -316,29 +282,10 @@ protected override async IAsyncEnumerable RunCoreStreamingA string prompt = string.Join("\n", messages.Select(m => m.Text)); // Handle DataContent as attachments - List? 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(), $"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) - }); - } - } - } + List? attachments = await ProcessDataContentAttachmentsAsync( + messages, + tempFiles, + cancellationToken).ConfigureAwait(false); // Send the message with attachments MessageOptions messageOptions = new() { Prompt = prompt }; @@ -358,22 +305,7 @@ protected override async IAsyncEnumerable RunCoreStreamingA finally { subscription.Dispose(); - - // Clean up temp files - foreach (string tempFile in tempFiles) - { - try - { - if (File.Exists(tempFile)) - { - File.Delete(tempFile); - } - } - catch - { - // Best effort cleanup - } - } + CleanupTempFiles(tempFiles); } } finally @@ -459,4 +391,54 @@ private static string GetExtensionForMediaType(string? mediaType) _ => ".dat" }; } + + private static async Task?> ProcessDataContentAttachmentsAsync( + IEnumerable messages, + List tempFiles, + CancellationToken cancellationToken) + { + List? 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(), $"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 tempFiles) + { + foreach (string tempFile in tempFiles) + { + try + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + catch + { + // Best effort cleanup + } + } + } } From 67d7b11bc4767e98ab159bc727353f9a032ff978 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Sun, 25 Jan 2026 22:22:13 -0800 Subject: [PATCH 19/34] Updated sample and session config mapping --- dotnet/Directory.Packages.props | 3 +- .../Agent_With_GithubCopilot.csproj | 1 + .../Agent_With_GithubCopilot/Program.cs | 46 ++++++++++++++++--- .../GithubCopilotAgent.cs | 27 ++++++++++- 4 files changed, 68 insertions(+), 9 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 04e1ba6441..c73331d748 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -89,7 +89,8 @@ - + + diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Agent_With_GithubCopilot.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Agent_With_GithubCopilot.csproj index 69d7086d5e..e6c0eefb3f 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Agent_With_GithubCopilot.csproj +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Agent_With_GithubCopilot.csproj @@ -10,6 +10,7 @@ + diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs index 4a6c5d2f08..7f89a8634d 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs @@ -1,17 +1,51 @@ // Copyright (c) Microsoft. All rights reserved. -// This sample shows how to create and use a simple AI agent with GitHub Copilot SDK. +// 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 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(new CopilotClientOptions { AutoStart = true }); await copilotClient.StartAsync(); -// Create an instance of the AIAgent using the extension method -AIAgent agent = copilotClient.AsAIAgent(ownsClient: true); +// 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); + } -// Ask Copilot to write code for us - demonstrate its code generation capabilities -AgentResponse response = await agent.RunAsync("Write a small .NET 10 C# hello world single file application"); -Console.WriteLine(response); + Console.WriteLine(); +} +else +{ + AgentResponse response = await agent.RunAsync(prompt); + Console.WriteLine(response); +} diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index 78a4d2383a..b5030a5884 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -122,7 +122,10 @@ protected override async Task RunCoreAsync( CopilotSession session; if (typedThread.SessionId is not null) { - session = await this._copilotClient.ResumeSessionAsync(typedThread.SessionId, cancellationToken: cancellationToken).ConfigureAwait(false); + session = await this._copilotClient.ResumeSessionAsync( + typedThread.SessionId, + this.CreateResumeConfig(), + cancellationToken).ConfigureAwait(false); } else { @@ -228,6 +231,11 @@ protected override async IAsyncEnumerable RunCoreStreamingA 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 }; @@ -237,7 +245,7 @@ protected override async IAsyncEnumerable RunCoreStreamingA { session = await this._copilotClient.ResumeSessionAsync( typedThread.SessionId, - new ResumeSessionConfig { Streaming = true }, + this.CreateResumeConfig(streaming: true), cancellationToken).ConfigureAwait(false); } else @@ -343,6 +351,21 @@ private async Task EnsureClientStartedAsync(CancellationToken cancellationToken) } } + private ResumeSessionConfig CreateResumeConfig(bool streaming = false) + { + 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 = streaming, + }; + } + private ChatMessage ConvertToChatMessage(AssistantMessageEvent assistantMessage) { return new ChatMessage(ChatRole.Assistant, assistantMessage.Data?.Content ?? string.Empty) From efef0548634d01cca666f77274342771774662fa Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Sun, 25 Jan 2026 23:02:01 -0800 Subject: [PATCH 20/34] Added instructions parameter --- .../CopilotClientExtensions.cs | 6 ++++-- .../GithubCopilotAgent.cs | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs index e7cd75baef..ef0bb7a8fd 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs @@ -47,12 +47,13 @@ public static AIAgent AsAIAgent( } /// - /// Retrieves an instance of for a GitHub Copilot client with tools. + /// Retrieves an instance of for a GitHub Copilot client. /// /// The to use for the agent. /// The tools to make available to the agent. /// Whether the agent owns the client and should dispose it. Default is false. /// The unique identifier for the agent. + /// Optional instructions to append as a system message. /// The name of the agent. /// The description of the agent. /// An instance backed by the GitHub Copilot client. @@ -61,11 +62,12 @@ public static AIAgent AsAIAgent( IList? tools, bool ownsClient = false, string? id = null, + string? instructions = null, string? name = null, string? description = null) { Throw.IfNull(client); - return new GithubCopilotAgent(client, tools, ownsClient, id, name, description); + return new GithubCopilotAgent(client, tools, ownsClient, id, instructions, name, description); } } diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index b5030a5884..ed267b6d08 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -54,12 +54,13 @@ public GithubCopilotAgent( } /// - /// Initializes a new instance of the class with tools. + /// Initializes a new instance of the class. /// /// The Copilot client to use for interacting with GitHub Copilot. /// The tools to make available to the agent. /// Whether the agent owns the client and should dispose it. Default is false. /// The unique identifier for the agent. + /// Optional instructions to append as a system message. /// The name of the agent. /// The description of the agent. public GithubCopilotAgent( @@ -67,11 +68,12 @@ public GithubCopilotAgent( IList? tools, bool ownsClient = false, string? id = null, + string? instructions = null, string? name = null, string? description = null) : this( copilotClient, - tools is { Count: > 0 } ? new SessionConfig { Tools = tools.OfType().ToList() } : null, + GetSessionConfig(tools, instructions), ownsClient, id, name, @@ -396,6 +398,19 @@ private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent a }; } + private static SessionConfig? GetSessionConfig(IList? tools, string? instructions) + { + List? mappedTools = tools is { Count: > 0 } ? tools.OfType().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 string GetExtensionForMediaType(string? mediaType) { return (mediaType?.ToUpperInvariant()) switch From 7c9d36c5ce69bf01a90842b8c5d26aafb354c16f Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Sun, 25 Jan 2026 23:21:51 -0800 Subject: [PATCH 21/34] Updated README --- .../AgentProviders/Agent_With_GithubCopilot/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md index 56610577b5..abffbd471f 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md @@ -50,7 +50,7 @@ await using CopilotClient copilotClient = new(new CopilotClientOptions await copilotClient.StartAsync(); // Create session configuration with specific model -var sessionConfig = new SessionConfig +SessionConfig sessionConfig = new() { Model = "gpt-4", Streaming = false @@ -75,7 +75,7 @@ Console.WriteLine(response); To get streaming responses: ```csharp -await foreach (var update in agent.RunStreamingAsync("Tell me a story")) +await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("Tell me a story")) { Console.Write(update.Text); } From 5b07a21bbb9055b2b63add801dcddf8735e1aa11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:39:46 +0000 Subject: [PATCH 22/34] 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> --- dotnet/Directory.Packages.props | 1 - .../Agent_With_GithubCopilot/README.md | 4 +- .../CopilotClientExtensions.cs | 8 +- .../GithubCopilotAgent.cs | 47 ++++++----- .../Microsoft.Agents.AI.GithubCopilot.csproj | 4 - .../GithubCopilotAgentThreadTests.cs | 78 ------------------- 6 files changed, 33 insertions(+), 109 deletions(-) delete mode 100644 dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index c73331d748..cbddb41dad 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -90,7 +90,6 @@ - diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md index abffbd471f..2de236a651 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md @@ -65,8 +65,8 @@ AIAgent agent = copilotClient.AsAIAgent( description: "A helpful AI assistant powered by GitHub Copilot" ); -// Use the agent -AgentResponse response = await agent.RunAsync("What is the weather like today?"); +// 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); ``` diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs index ef0bb7a8fd..f4b89907d1 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs @@ -53,21 +53,21 @@ public static AIAgent AsAIAgent( /// The tools to make available to the agent. /// Whether the agent owns the client and should dispose it. Default is false. /// The unique identifier for the agent. - /// Optional instructions to append as a system message. /// The name of the agent. /// The description of the agent. + /// Optional instructions to append as a system message. /// An instance backed by the GitHub Copilot client. public static AIAgent AsAIAgent( this CopilotClient client, IList? tools, bool ownsClient = false, string? id = null, - string? instructions = null, string? name = null, - string? description = null) + string? description = null, + string? instructions = null) { Throw.IfNull(client); - return new GithubCopilotAgent(client, tools, ownsClient, id, instructions, name, description); + return new GithubCopilotAgent(client, tools, ownsClient, id, name, description, instructions); } } diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index ed267b6d08..567718a830 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -60,24 +60,24 @@ public GithubCopilotAgent( /// The tools to make available to the agent. /// Whether the agent owns the client and should dispose it. Default is false. /// The unique identifier for the agent. - /// Optional instructions to append as a system message. /// The name of the agent. /// The description of the agent. + /// Optional instructions to append as a system message. public GithubCopilotAgent( CopilotClient copilotClient, IList? tools, bool ownsClient = false, string? id = null, - string? instructions = null, string? name = null, - string? description = null) + string? description = null, + string? instructions = null) : this( copilotClient, GetSessionConfig(tools, instructions), ownsClient, id, - name, - description) + name ?? "GitHub Copilot Agent", + description ?? "An AI agent powered by GitHub Copilot SDK") { } @@ -411,23 +411,30 @@ private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent a return new SessionConfig { Tools = mappedTools, SystemMessage = systemMessage }; } + private static readonly Dictionary 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?.ToUpperInvariant()) switch + if (string.IsNullOrEmpty(mediaType)) { - "IMAGE/PNG" => ".png", - "IMAGE/JPEG" or "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", - _ => ".dat" - }; + return ".dat"; + } + + return MediaTypeExtensions.TryGetValue(mediaType, out string? extension) ? extension : ".dat"; } private static async Task?> ProcessDataContentAttachmentsAsync( @@ -443,7 +450,7 @@ private static string GetExtensionForMediaType(string? mediaType) if (content is DataContent dataContent) { // Write DataContent to a temp file - string tempFilePath = Path.Combine(Path.GetTempPath(), $"copilot_data_{Guid.NewGuid()}{GetExtensionForMediaType(dataContent.MediaType)}"); + 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); diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/Microsoft.Agents.AI.GithubCopilot.csproj b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/Microsoft.Agents.AI.GithubCopilot.csproj index e752bd543e..f4f79c27bd 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/Microsoft.Agents.AI.GithubCopilot.csproj +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/Microsoft.Agents.AI.GithubCopilot.csproj @@ -27,8 +27,4 @@ Provides Microsoft Agent Framework support for GitHub Copilot SDK. - - - - diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs deleted file mode 100644 index 7101d3a0b6..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentThreadTests.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json; - -namespace Microsoft.Agents.AI.GithubCopilot.UnitTests; - -/// -/// Unit tests for the class. -/// -public sealed class GithubCopilotAgentThreadTests -{ - [Fact] - public void Constructor_InitializesWithNullSessionId() - { - // Act - var thread = new GithubCopilotAgentThread(); - - // Assert - Assert.Null(thread.SessionId); - } - - [Fact] - public void SessionId_IsInternalSet() - { - // Arrange - const string Json = """{"sessionId":"test-value"}"""; - JsonDocument doc = JsonDocument.Parse(Json); - - // Act - var thread = new GithubCopilotAgentThread(doc.RootElement); - - // Assert - Assert.Equal("test-value", thread.SessionId); - } - - [Fact] - public void Constructor_RoundTrip_SerializationPreservesState() - { - // Arrange - const string SessionId = "session-rt-001"; - GithubCopilotAgentThread originalThread = new() { SessionId = SessionId }; - - // Act - JsonElement serialized = originalThread.Serialize(); - GithubCopilotAgentThread deserializedThread = new(serialized); - - // Assert - Assert.Equal(originalThread.SessionId, deserializedThread.SessionId); - } - - [Fact] - public void Deserialize_WithSessionId_DeserializesCorrectly() - { - // Arrange - const string Json = """{"sessionId":"test-session-id"}"""; - JsonDocument doc = JsonDocument.Parse(Json); - - // Act - var thread = new GithubCopilotAgentThread(doc.RootElement); - - // Assert - Assert.Equal("test-session-id", thread.SessionId); - } - - [Fact] - public void Deserialize_WithoutSessionId_HasNullSessionId() - { - // Arrange - const string Json = """{}"""; - JsonDocument doc = JsonDocument.Parse(Json); - - // Act - var thread = new GithubCopilotAgentThread(doc.RootElement); - - // Assert - Assert.Null(thread.SessionId); - } -} From 8ec23a18d8c564bd3cc3475f180e876ceda17207 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 13:18:54 +0000 Subject: [PATCH 23/34] Remove StreamJsonRpc reference from sample project Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Agent_With_GithubCopilot/Agent_With_GithubCopilot.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Agent_With_GithubCopilot.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Agent_With_GithubCopilot.csproj index e6c0eefb3f..69d7086d5e 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Agent_With_GithubCopilot.csproj +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Agent_With_GithubCopilot.csproj @@ -10,7 +10,6 @@ - From 251ad7c29d4f56c8b74181e096e50d66ceb353af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:26:46 +0000 Subject: [PATCH 24/34] 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> --- .../Agent_With_GithubCopilot/README.md | 12 +++--------- .../CopilotClientExtensions.cs | 6 +++--- .../GithubCopilotAgent.cs | 15 +++++---------- .../CopilotClientExtensionsTests.cs | 10 +++++----- .../GithubCopilotAgentTests.cs | 16 ++++++++-------- 5 files changed, 24 insertions(+), 35 deletions(-) diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md index 2de236a651..6065abec1d 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md @@ -39,14 +39,8 @@ You can customize the agent by providing additional configuration: using GitHub.Copilot.SDK; using Microsoft.Agents.AI; -// Create a Copilot client with custom options -await using CopilotClient copilotClient = new(new CopilotClientOptions -{ - CliPath = "/custom/path/to/copilot", // Custom CLI path - LogLevel = "debug", // Enable debug logging - AutoStart = true -}); - +// Create and start a Copilot client +await using CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = true }); await copilotClient.StartAsync(); // Create session configuration with specific model @@ -75,7 +69,7 @@ Console.WriteLine(response); To get streaming responses: ```csharp -await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("Tell me a story")) +await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("Write a Python function to calculate Fibonacci numbers")) { Console.Write(update.Text); } diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs index f4b89907d1..ea20d46c46 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/CopilotClientExtensions.cs @@ -50,24 +50,24 @@ public static AIAgent AsAIAgent( /// Retrieves an instance of for a GitHub Copilot client. /// /// The to use for the agent. - /// The tools to make available to the agent. /// Whether the agent owns the client and should dispose it. Default is false. /// The unique identifier for the agent. /// The name of the agent. /// The description of the agent. + /// The tools to make available to the agent. /// Optional instructions to append as a system message. /// An instance backed by the GitHub Copilot client. public static AIAgent AsAIAgent( this CopilotClient client, - IList? tools, bool ownsClient = false, string? id = null, string? name = null, string? description = null, + IList? tools = null, string? instructions = null) { Throw.IfNull(client); - return new GithubCopilotAgent(client, tools, ownsClient, id, name, description, instructions); + return new GithubCopilotAgent(client, ownsClient, id, name, description, tools, instructions); } } diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index 567718a830..5996a8f9b9 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -57,19 +57,19 @@ public GithubCopilotAgent( /// Initializes a new instance of the class. /// /// The Copilot client to use for interacting with GitHub Copilot. - /// The tools to make available to the agent. /// Whether the agent owns the client and should dispose it. Default is false. /// The unique identifier for the agent. /// The name of the agent. /// The description of the agent. + /// The tools to make available to the agent. /// Optional instructions to append as a system message. public GithubCopilotAgent( CopilotClient copilotClient, - IList? tools, bool ownsClient = false, string? id = null, string? name = null, string? description = null, + IList? tools = null, string? instructions = null) : this( copilotClient, @@ -77,7 +77,7 @@ public GithubCopilotAgent( ownsClient, id, name ?? "GitHub Copilot Agent", - description ?? "An AI agent powered by GitHub Copilot SDK") + description ?? "An AI agent powered by GitHub Copilot") { } @@ -411,7 +411,7 @@ private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent a return new SessionConfig { Tools = mappedTools, SystemMessage = systemMessage }; } - private static readonly Dictionary MediaTypeExtensions = new(StringComparer.OrdinalIgnoreCase) + private static readonly Dictionary s_mediaTypeExtensions = new(StringComparer.OrdinalIgnoreCase) { ["image/png"] = ".png", ["image/jpeg"] = ".jpg", @@ -429,12 +429,7 @@ private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent a private static string GetExtensionForMediaType(string? mediaType) { - if (string.IsNullOrEmpty(mediaType)) - { - return ".dat"; - } - - return MediaTypeExtensions.TryGetValue(mediaType, out string? extension) ? extension : ".dat"; + return mediaType is not null && s_mediaTypeExtensions.TryGetValue(mediaType, out string? extension) ? extension : ".dat"; } private static async Task?> ProcessDataContentAttachmentsAsync( diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/CopilotClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/CopilotClientExtensionsTests.cs index 662c839866..5c3483f4b2 100644 --- a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/CopilotClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/CopilotClientExtensionsTests.cs @@ -23,7 +23,7 @@ public void AsAIAgent_WithAllParameters_ReturnsGithubCopilotAgentWithSpecifiedPr const string TestDescription = "This is a test agent description"; // Act - var agent = copilotClient.AsAIAgent(id: TestId, name: TestName, description: TestDescription); + var agent = copilotClient.AsAIAgent(ownsClient: false, id: TestId, name: TestName, description: TestDescription, tools: null); // Assert Assert.NotNull(agent); @@ -40,7 +40,7 @@ public void AsAIAgent_WithMinimalParameters_ReturnsGithubCopilotAgent() CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); // Act - var agent = copilotClient.AsAIAgent(); + var agent = copilotClient.AsAIAgent(ownsClient: false, tools: null); // Assert Assert.NotNull(agent); @@ -54,7 +54,7 @@ public void AsAIAgent_WithNullClient_ThrowsArgumentNullException() CopilotClient? copilotClient = null; // Act & Assert - Assert.Throws(() => copilotClient!.AsAIAgent()); + Assert.Throws(() => copilotClient!.AsAIAgent(sessionConfig: null)); } [Fact] @@ -64,7 +64,7 @@ public void AsAIAgent_WithOwnsClient_ReturnsAgentThatOwnsClient() CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); // Act - var agent = copilotClient.AsAIAgent(ownsClient: true); + var agent = copilotClient.AsAIAgent(ownsClient: true, tools: null); // Assert Assert.NotNull(agent); @@ -79,7 +79,7 @@ public void AsAIAgent_WithTools_ReturnsAgentWithTools() List tools = [AIFunctionFactory.Create(() => "test", "TestFunc", "Test function")]; // Act - var agent = copilotClient.AsAIAgent(tools); + var agent = copilotClient.AsAIAgent(tools: tools); // Assert Assert.NotNull(agent); diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs index bee14759cd..2e68b4213e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs @@ -23,7 +23,7 @@ public void Constructor_WithCopilotClient_InitializesPropertiesCorrectly() const string TestDescription = "test-description"; // Act - var agent = new GithubCopilotAgent(copilotClient, id: TestId, name: TestName, description: TestDescription); + var agent = new GithubCopilotAgent(copilotClient, ownsClient: false, id: TestId, name: TestName, description: TestDescription, tools: null); // Assert Assert.Equal(TestId, agent.Id); @@ -35,7 +35,7 @@ public void Constructor_WithCopilotClient_InitializesPropertiesCorrectly() public void Constructor_WithNullCopilotClient_ThrowsArgumentNullException() { // Act & Assert - Assert.Throws(() => new GithubCopilotAgent(null!)); + Assert.Throws(() => new GithubCopilotAgent(copilotClient: null!, sessionConfig: null)); } [Fact] @@ -45,13 +45,13 @@ public void Constructor_WithDefaultParameters_UsesBaseProperties() CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); // Act - var agent = new GithubCopilotAgent(copilotClient); + var agent = new GithubCopilotAgent(copilotClient, ownsClient: false, tools: null); // Assert Assert.NotNull(agent.Id); Assert.NotEmpty(agent.Id); - Assert.Null(agent.Name); - Assert.Null(agent.Description); + Assert.Equal("GitHub Copilot Agent", agent.Name); + Assert.Equal("An AI agent powered by GitHub Copilot", agent.Description); } [Fact] @@ -59,7 +59,7 @@ public async Task GetNewThreadAsync_ReturnsGithubCopilotAgentThreadAsync() { // Arrange CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); - var agent = new GithubCopilotAgent(copilotClient); + var agent = new GithubCopilotAgent(copilotClient, ownsClient: false, tools: null); // Act var thread = await agent.GetNewThreadAsync(); @@ -74,7 +74,7 @@ public async Task GetNewThreadAsync_WithSessionId_ReturnsThreadWithSessionIdAsyn { // Arrange CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); - var agent = new GithubCopilotAgent(copilotClient); + var agent = new GithubCopilotAgent(copilotClient, ownsClient: false, tools: null); const string TestSessionId = "test-session-id"; // Act @@ -94,7 +94,7 @@ public void Constructor_WithTools_InitializesCorrectly() List tools = [AIFunctionFactory.Create(() => "test", "TestFunc", "Test function")]; // Act - var agent = new GithubCopilotAgent(copilotClient, tools); + var agent = new GithubCopilotAgent(copilotClient, tools: tools); // Assert Assert.NotNull(agent); From ced729948d99d8789e70065af4fa75c9d5fe202c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:40:56 +0000 Subject: [PATCH 25/34] Fix streaming prompt: change Python to C# for Fibonacci example Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../AgentProviders/Agent_With_GithubCopilot/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md index 6065abec1d..fe7dcb1a58 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md @@ -69,7 +69,7 @@ Console.WriteLine(response); To get streaming responses: ```csharp -await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("Write a Python function to calculate Fibonacci numbers")) +await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("Write a C# function to calculate Fibonacci numbers")) { Console.Write(update.Text); } From 396d0aacf985bd21a3195465af227a1e9a60ba7c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:33:35 +0000 Subject: [PATCH 26/34] 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> --- .../Agent_With_GithubCopilot/Program.cs | 4 +- .../Agent_With_GithubCopilot/README.md | 2 +- .../GithubCopilotAgent.cs | 65 +++++++++++++++++-- 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs index 7f89a8634d..6cb178c2ca 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs @@ -17,8 +17,8 @@ static Task PromptPermission(PermissionRequest request, return Task.FromResult(new PermissionRequestResult { Kind = kind }); } -// Create and start a Copilot client -await using CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = true }); +// Create and start a Copilot client (AutoStart defaults to true) +await using CopilotClient copilotClient = new(new CopilotClientOptions()); await copilotClient.StartAsync(); // Create an agent with a session config that enables permission handling diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md index fe7dcb1a58..1ad38acc7c 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md @@ -46,7 +46,7 @@ await copilotClient.StartAsync(); // Create session configuration with specific model SessionConfig sessionConfig = new() { - Model = "gpt-4", + Model = "claude-opus-4.5", Streaming = false }; diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index 5996a8f9b9..8b4eb72ed6 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -7,6 +7,7 @@ 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; @@ -258,10 +259,10 @@ protected override async IAsyncEnumerable RunCoreStreamingA try { - System.Threading.Channels.Channel channel = System.Threading.Channels.Channel.CreateUnbounded(); + Channel channel = Channel.CreateUnbounded(); // Subscribe to session events - IDisposable subscription = session.On(evt => + using IDisposable subscription = session.On(evt => { switch (evt) { @@ -273,6 +274,10 @@ protected override async IAsyncEnumerable RunCoreStreamingA channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(assistantMessage)); break; + case AssistantUsageEvent usageEvent: + channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(usageEvent)); + break; + case SessionIdleEvent: channel.Writer.TryComplete(); break; @@ -282,6 +287,11 @@ protected override async IAsyncEnumerable RunCoreStreamingA $"Session error: {errorEvent.Data?.Message ?? "Unknown error"}"); channel.Writer.TryComplete(exception); break; + + default: + // Handle all other event types by storing as RawRepresentation + channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(evt)); + break; } }); @@ -305,7 +315,6 @@ protected override async IAsyncEnumerable RunCoreStreamingA } await session.SendAsync(messageOptions, cancellationToken).ConfigureAwait(false); - // Yield updates as they arrive await foreach (AgentResponseUpdate update in channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { @@ -379,7 +388,12 @@ private ChatMessage ConvertToChatMessage(AssistantMessageEvent assistantMessage) private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageDeltaEvent deltaEvent) { - return new AgentResponseUpdate(ChatRole.Assistant, [new TextContent(deltaEvent.Data?.DeltaContent ?? string.Empty)]) + TextContent textContent = new(deltaEvent.Data?.DeltaContent ?? string.Empty) + { + RawRepresentation = deltaEvent + }; + + return new AgentResponseUpdate(ChatRole.Assistant, [textContent]) { AgentId = this.Id, MessageId = deltaEvent.Data?.MessageId, @@ -389,7 +403,12 @@ private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageDeltaEv private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent assistantMessage) { - return new AgentResponseUpdate(ChatRole.Assistant, [new TextContent(assistantMessage.Data?.Content ?? string.Empty)]) + TextContent textContent = new(assistantMessage.Data?.Content ?? string.Empty) + { + RawRepresentation = assistantMessage + }; + + return new AgentResponseUpdate(ChatRole.Assistant, [textContent]) { AgentId = this.Id, ResponseId = assistantMessage.Data?.MessageId, @@ -398,6 +417,42 @@ private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent a }; } + 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)) + }; + + UsageContent usageContent = new(usageDetails) + { + RawRepresentation = usageEvent + }; + + return new AgentResponseUpdate(ChatRole.Assistant, [usageContent]) + { + AgentId = this.Id, + CreatedAt = usageEvent.Timestamp + }; + } + + 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? tools, string? instructions) { List? mappedTools = tools is { Count: > 0 } ? tools.OfType().ToList() : null; From 77b50d75761def053d3563824a811fd6f0ba96ea Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:18:49 -0800 Subject: [PATCH 27/34] Resolved build errors --- .../GithubCopilotAgent.cs | 74 +++++++++---------- ...Thread.cs => GithubCopilotAgentSession.cs} | 12 +-- .../GithubCopilotJsonUtilities.cs | 2 +- .../GithubCopilotAgentTests.cs | 18 ++--- 4 files changed, 53 insertions(+), 53 deletions(-) rename dotnet/src/Microsoft.Agents.AI.GithubCopilot/{GithubCopilotAgentThread.cs => GithubCopilotAgentSession.cs} (79%) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index 8b4eb72ed6..95d58d50b4 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -83,57 +83,57 @@ public GithubCopilotAgent( } /// - public sealed override ValueTask GetNewThreadAsync(CancellationToken cancellationToken = default) - => new(new GithubCopilotAgentThread()); + public sealed override ValueTask GetNewSessionAsync(CancellationToken cancellationToken = default) + => new(new GithubCopilotAgentSession()); /// - /// Get a new instance using an existing session id, to continue that conversation. + /// Get a new instance using an existing session id, to continue that conversation. /// /// The session id to continue. - /// A new instance. - public ValueTask GetNewThreadAsync(string sessionId) - => new(new GithubCopilotAgentThread() { SessionId = sessionId }); + /// A new instance. + public ValueTask GetNewSessionAsync(string sessionId) + => new(new GithubCopilotAgentSession() { SessionId = sessionId }); /// - public override ValueTask DeserializeThreadAsync( - JsonElement serializedThread, + public override ValueTask DeserializeSessionAsync( + JsonElement serializedSession, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) - => new(new GithubCopilotAgentThread(serializedThread, jsonSerializerOptions)); + => new(new GithubCopilotAgentSession(serializedSession, jsonSerializerOptions)); /// protected override async Task RunCoreAsync( IEnumerable messages, - AgentThread? thread = null, + AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { _ = Throw.IfNull(messages); - // Ensure we have a valid thread - thread ??= await this.GetNewThreadAsync(cancellationToken).ConfigureAwait(false); - if (thread is not GithubCopilotAgentThread typedThread) + // Ensure we have a valid session + session ??= await this.GetNewSessionAsync(cancellationToken).ConfigureAwait(false); + if (session is not GithubCopilotAgentSession typedSession) { throw new InvalidOperationException( - $"The provided thread type {thread.GetType()} is not compatible with the agent. Only GitHub Copilot agent created threads are supported."); + $"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 - CopilotSession session; - if (typedThread.SessionId is not null) + CopilotSession copilotSession; + if (typedSession.SessionId is not null) { - session = await this._copilotClient.ResumeSessionAsync( - typedThread.SessionId, + copilotSession = await this._copilotClient.ResumeSessionAsync( + typedSession.SessionId, this.CreateResumeConfig(), cancellationToken).ConfigureAwait(false); } else { - session = await this._copilotClient.CreateSessionAsync(this._sessionConfig, cancellationToken).ConfigureAwait(false); - typedThread.SessionId = session.SessionId; + copilotSession = await this._copilotClient.CreateSessionAsync(this._sessionConfig, cancellationToken).ConfigureAwait(false); + typedSession.SessionId = copilotSession.SessionId; } try @@ -143,7 +143,7 @@ protected override async Task RunCoreAsync( TaskCompletionSource completionSource = new(); // Subscribe to session events - IDisposable subscription = session.On(evt => + IDisposable subscription = copilotSession.On(evt => { switch (evt) { @@ -181,7 +181,7 @@ protected override async Task RunCoreAsync( messageOptions.Attachments = [.. attachments]; } - await session.SendAsync(messageOptions, cancellationToken).ConfigureAwait(false); + await copilotSession.SendAsync(messageOptions, cancellationToken).ConfigureAwait(false); // Wait for completion await completionSource.Task.ConfigureAwait(false); @@ -200,25 +200,25 @@ protected override async Task RunCoreAsync( } finally { - await session.DisposeAsync().ConfigureAwait(false); + await copilotSession.DisposeAsync().ConfigureAwait(false); } } /// protected override async IAsyncEnumerable RunCoreStreamingAsync( IEnumerable messages, - AgentThread? thread = null, + AgentSession? session = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { _ = Throw.IfNull(messages); - // Ensure we have a valid thread - thread ??= await this.GetNewThreadAsync(cancellationToken).ConfigureAwait(false); - if (thread is not GithubCopilotAgentThread typedThread) + // Ensure we have a valid session + session ??= await this.GetNewSessionAsync(cancellationToken).ConfigureAwait(false); + if (session is not GithubCopilotAgentSession typedSession) { throw new InvalidOperationException( - $"The provided thread type {thread.GetType()} is not compatible with the agent. Only GitHub Copilot agent created threads are supported."); + $"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 @@ -243,18 +243,18 @@ protected override async IAsyncEnumerable RunCoreStreamingA } : new SessionConfig { Streaming = true }; - CopilotSession session; - if (typedThread.SessionId is not null) + CopilotSession copilotSession; + if (typedSession.SessionId is not null) { - session = await this._copilotClient.ResumeSessionAsync( - typedThread.SessionId, + copilotSession = await this._copilotClient.ResumeSessionAsync( + typedSession.SessionId, this.CreateResumeConfig(streaming: true), cancellationToken).ConfigureAwait(false); } else { - session = await this._copilotClient.CreateSessionAsync(sessionConfig, cancellationToken).ConfigureAwait(false); - typedThread.SessionId = session.SessionId; + copilotSession = await this._copilotClient.CreateSessionAsync(sessionConfig, cancellationToken).ConfigureAwait(false); + typedSession.SessionId = copilotSession.SessionId; } try @@ -262,7 +262,7 @@ protected override async IAsyncEnumerable RunCoreStreamingA Channel channel = Channel.CreateUnbounded(); // Subscribe to session events - using IDisposable subscription = session.On(evt => + using IDisposable subscription = copilotSession.On(evt => { switch (evt) { @@ -314,7 +314,7 @@ protected override async IAsyncEnumerable RunCoreStreamingA messageOptions.Attachments = [.. attachments]; } - await session.SendAsync(messageOptions, cancellationToken).ConfigureAwait(false); + await copilotSession.SendAsync(messageOptions, cancellationToken).ConfigureAwait(false); // Yield updates as they arrive await foreach (AgentResponseUpdate update in channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { @@ -329,7 +329,7 @@ protected override async IAsyncEnumerable RunCoreStreamingA } finally { - await session.DisposeAsync().ConfigureAwait(false); + await copilotSession.DisposeAsync().ConfigureAwait(false); } } diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentSession.cs similarity index 79% rename from dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs rename to dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentSession.cs index 5e31d25227..3c7d9a6598 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentThread.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgentSession.cs @@ -5,9 +5,9 @@ namespace Microsoft.Agents.AI.GithubCopilot; /// -/// Represents a thread for a GitHub Copilot agent conversation. +/// Represents a session for a GitHub Copilot agent conversation. /// -public sealed class GithubCopilotAgentThread : AgentThread +public sealed class GithubCopilotAgentSession : AgentSession { /// /// Gets or sets the session ID for the GitHub Copilot conversation. @@ -15,18 +15,18 @@ public sealed class GithubCopilotAgentThread : AgentThread public string? SessionId { get; internal set; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - internal GithubCopilotAgentThread() + internal GithubCopilotAgentSession() { } /// - /// Initializes a new instance of the class from serialized data. + /// Initializes a new instance of the class from serialized data. /// /// The serialized thread data. /// Optional JSON serialization options. - internal GithubCopilotAgentThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) + internal GithubCopilotAgentSession(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) { // The JSON serialization uses camelCase if (serializedThread.TryGetProperty("sessionId", out JsonElement sessionIdElement)) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs index 14f6288d64..c5a6e24d8e 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotJsonUtilities.cs @@ -42,7 +42,7 @@ private static JsonSerializerOptions CreateDefaultOptions() UseStringEnumConverter = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, NumberHandling = JsonNumberHandling.AllowReadingFromString)] - [JsonSerializable(typeof(GithubCopilotAgentThread.State))] + [JsonSerializable(typeof(GithubCopilotAgentSession.State))] [ExcludeFromCodeCoverage] private sealed partial class JsonContext : JsonSerializerContext; } diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs index 2e68b4213e..90b84fb3bd 100644 --- a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/GithubCopilotAgentTests.cs @@ -55,22 +55,22 @@ public void Constructor_WithDefaultParameters_UsesBaseProperties() } [Fact] - public async Task GetNewThreadAsync_ReturnsGithubCopilotAgentThreadAsync() + public async Task GetNewSessionAsync_ReturnsGithubCopilotAgentSessionAsync() { // Arrange CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); var agent = new GithubCopilotAgent(copilotClient, ownsClient: false, tools: null); // Act - var thread = await agent.GetNewThreadAsync(); + var session = await agent.GetNewSessionAsync(); // Assert - Assert.NotNull(thread); - Assert.IsType(thread); + Assert.NotNull(session); + Assert.IsType(session); } [Fact] - public async Task GetNewThreadAsync_WithSessionId_ReturnsThreadWithSessionIdAsync() + public async Task GetNewSessionAsync_WithSessionId_ReturnsSessionWithSessionIdAsync() { // Arrange CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); @@ -78,12 +78,12 @@ public async Task GetNewThreadAsync_WithSessionId_ReturnsThreadWithSessionIdAsyn const string TestSessionId = "test-session-id"; // Act - var thread = await agent.GetNewThreadAsync(TestSessionId); + var session = await agent.GetNewSessionAsync(TestSessionId); // Assert - Assert.NotNull(thread); - var typedThread = Assert.IsType(thread); - Assert.Equal(TestSessionId, typedThread.SessionId); + Assert.NotNull(session); + var typedSession = Assert.IsType(session); + Assert.Equal(TestSessionId, typedSession.SessionId); } [Fact] From 9cb4c1f96095e9b08d94a77c06a6603b05846222 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:47:35 -0800 Subject: [PATCH 28/34] Addressed comments --- .../Agent_With_GithubCopilot/README.md | 2 +- .../GithubCopilotAgent.cs | 63 ++++++++++++++----- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md index 1ad38acc7c..15f85baffd 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md @@ -40,7 +40,7 @@ using GitHub.Copilot.SDK; using Microsoft.Agents.AI; // Create and start a Copilot client -await using CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = true }); +await using CopilotClient copilotClient = new(new CopilotClientOptions()); await copilotClient.StartAsync(); // Create session configuration with specific model diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index 95d58d50b4..b629bea882 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -20,10 +20,13 @@ namespace Microsoft.Agents.AI.GithubCopilot; /// 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 string _name; + private readonly string _description; private readonly SessionConfig? _sessionConfig; private readonly bool _ownsClient; @@ -50,8 +53,8 @@ public GithubCopilotAgent( this._sessionConfig = sessionConfig; this._ownsClient = ownsClient; this._id = id; - this._name = name; - this._description = description; + this._name = name ?? DefaultName; + this._description = description ?? DefaultDescription; } /// @@ -77,8 +80,8 @@ public GithubCopilotAgent( GetSessionConfig(tools, instructions), ownsClient, id, - name ?? "GitHub Copilot Agent", - description ?? "An AI agent powered by GitHub Copilot") + name, + description) { } @@ -278,14 +281,15 @@ protected override async IAsyncEnumerable RunCoreStreamingA channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(usageEvent)); break; - case SessionIdleEvent: + case SessionIdleEvent idleEvent: + channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(idleEvent)); channel.Writer.TryComplete(); break; case SessionErrorEvent errorEvent: - Exception exception = new InvalidOperationException( - $"Session error: {errorEvent.Data?.Message ?? "Unknown error"}"); - channel.Writer.TryComplete(exception); + channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(errorEvent)); + channel.Writer.TryComplete(new InvalidOperationException( + $"Session error: {errorEvent.Data?.Message ?? "Unknown error"}")); break; default: @@ -323,7 +327,6 @@ protected override async IAsyncEnumerable RunCoreStreamingA } finally { - subscription.Dispose(); CleanupTempFiles(tempFiles); } } @@ -337,10 +340,10 @@ protected override async IAsyncEnumerable RunCoreStreamingA protected override string? IdCore => this._id; /// - public override string? Name => this._name; + public override string Name => this._name; /// - public override string? Description => this._description; + public override string Description => this._description; /// /// Disposes the agent and releases resources. @@ -423,7 +426,9 @@ private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantUsageEvent usa { InputTokenCount = (int?)(usageEvent.Data?.InputTokens), OutputTokenCount = (int?)(usageEvent.Data?.OutputTokens), - TotalTokenCount = (int?)((usageEvent.Data?.InputTokens ?? 0) + (usageEvent.Data?.OutputTokens ?? 0)) + TotalTokenCount = (int?)((usageEvent.Data?.InputTokens ?? 0) + (usageEvent.Data?.OutputTokens ?? 0)), + CachedInputTokenCount = (int?)(usageEvent.Data?.CacheReadTokens), + AdditionalCounts = GetAdditionalCounts(usageEvent), }; UsageContent usageContent = new(usageDetails) @@ -438,6 +443,36 @@ private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantUsageEvent usa }; } + private static AdditionalPropertiesDictionary? GetAdditionalCounts(AssistantUsageEvent usageEvent) + { + if (usageEvent.Data is null) + { + return null; + } + + AdditionalPropertiesDictionary? additionalCounts = null; + + if (usageEvent.Data.CacheWriteTokens is double cacheWriteTokens) + { + additionalCounts ??= []; + additionalCounts["CacheWriteTokens"] = (long)cacheWriteTokens; + } + + if (usageEvent.Data.Cost is double cost) + { + additionalCounts ??= []; + additionalCounts["Cost"] = (long)cost; + } + + if (usageEvent.Data.Duration is double duration) + { + additionalCounts ??= []; + additionalCounts["Duration"] = (long)duration; + } + + return additionalCounts; + } + private AgentResponseUpdate ConvertToAgentResponseUpdate(SessionEvent sessionEvent) { // Handle arbitrary events by storing as RawRepresentation From 6e369015a6c44622d765b32ee0d7c6c275394888 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:49:44 -0800 Subject: [PATCH 29/34] Small fix --- .../AgentProviders/Agent_With_GithubCopilot/Program.cs | 4 ++-- .../AgentProviders/Agent_With_GithubCopilot/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs index 6cb178c2ca..b233259dcc 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Program.cs @@ -17,8 +17,8 @@ static Task PromptPermission(PermissionRequest request, return Task.FromResult(new PermissionRequestResult { Kind = kind }); } -// Create and start a Copilot client (AutoStart defaults to true) -await using CopilotClient copilotClient = new(new CopilotClientOptions()); +// 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 diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md index 15f85baffd..885988dbcb 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/README.md @@ -40,7 +40,7 @@ using GitHub.Copilot.SDK; using Microsoft.Agents.AI; // Create and start a Copilot client -await using CopilotClient copilotClient = new(new CopilotClientOptions()); +await using CopilotClient copilotClient = new(); await copilotClient.StartAsync(); // Create session configuration with specific model From 493536ba0f1153f1accd76ae6ae874fe9886c95b Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:41:28 -0800 Subject: [PATCH 30/34] Addressed comment --- .../GithubCopilotAgent.cs | 108 +----------------- 1 file changed, 2 insertions(+), 106 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index b629bea882..e4fbe29364 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -105,107 +105,12 @@ public override ValueTask DeserializeSessionAsync( => new(new GithubCopilotAgentSession(serializedSession, jsonSerializerOptions)); /// - protected override async Task RunCoreAsync( + protected override Task RunCoreAsync( IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, 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 - 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(this._sessionConfig, cancellationToken).ConfigureAwait(false); - typedSession.SessionId = copilotSession.SessionId; - } - - try - { - // Prepare to collect response - List responseMessages = []; - TaskCompletionSource completionSource = new(); - - // Subscribe to session events - IDisposable subscription = copilotSession.On(evt => - { - switch (evt) - { - case AssistantMessageEvent assistantMessage: - responseMessages.Add(this.ConvertToChatMessage(assistantMessage)); - break; - - case SessionIdleEvent: - completionSource.TrySetResult(true); - break; - - case SessionErrorEvent errorEvent: - completionSource.TrySetException(new InvalidOperationException( - $"Session error: {errorEvent.Data?.Message ?? "Unknown error"}")); - break; - } - }); - - List tempFiles = []; - try - { - // Build prompt from text content - string prompt = string.Join("\n", messages.Select(m => m.Text)); - - // Handle DataContent as attachments - List? 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); - - // Wait for completion - await completionSource.Task.ConfigureAwait(false); - - return new AgentResponse(responseMessages) - { - AgentId = this.Id, - ResponseId = responseMessages.LastOrDefault()?.MessageId, - }; - } - finally - { - subscription.Dispose(); - CleanupTempFiles(tempFiles); - } - } - finally - { - await copilotSession.DisposeAsync().ConfigureAwait(false); - } - } + => RunCoreStreamingAsync(messages, session, options, cancellationToken).ToAgentResponseAsync(cancellationToken); /// protected override async IAsyncEnumerable RunCoreStreamingAsync( @@ -380,15 +285,6 @@ private ResumeSessionConfig CreateResumeConfig(bool streaming = false) }; } - private ChatMessage ConvertToChatMessage(AssistantMessageEvent assistantMessage) - { - return new ChatMessage(ChatRole.Assistant, assistantMessage.Data?.Content ?? string.Empty) - { - MessageId = assistantMessage.Data?.MessageId, - CreatedAt = DateTimeOffset.UtcNow - }; - } - private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageDeltaEvent deltaEvent) { TextContent textContent = new(deltaEvent.Data?.DeltaContent ?? string.Empty) From 50f7c4709408507260c0143e70f311ee181425bb Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:43:56 -0800 Subject: [PATCH 31/34] Small fix --- .../src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index e4fbe29364..75636e35ea 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -110,7 +110,7 @@ protected override Task RunCoreAsync( AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) - => RunCoreStreamingAsync(messages, session, options, cancellationToken).ToAgentResponseAsync(cancellationToken); + => this.RunCoreStreamingAsync(messages, session, options, cancellationToken).ToAgentResponseAsync(cancellationToken); /// protected override async IAsyncEnumerable RunCoreStreamingAsync( From 0ae50a65f152fa73efaea609e67580d786923a00 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:20:01 -0800 Subject: [PATCH 32/34] Addressed comments --- .../GithubCopilotAgent.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs index 75636e35ea..481410b835 100644 --- a/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GithubCopilot/GithubCopilotAgent.cs @@ -156,7 +156,7 @@ protected override async IAsyncEnumerable RunCoreStreamingA { copilotSession = await this._copilotClient.ResumeSessionAsync( typedSession.SessionId, - this.CreateResumeConfig(streaming: true), + this.CreateResumeConfig(), cancellationToken).ConfigureAwait(false); } else @@ -270,7 +270,7 @@ private async Task EnsureClientStartedAsync(CancellationToken cancellationToken) } } - private ResumeSessionConfig CreateResumeConfig(bool streaming = false) + private ResumeSessionConfig CreateResumeConfig() { return new ResumeSessionConfig { @@ -281,7 +281,7 @@ private ResumeSessionConfig CreateResumeConfig(bool streaming = false) CustomAgents = this._sessionConfig?.CustomAgents, SkillDirectories = this._sessionConfig?.SkillDirectories, DisabledSkills = this._sessionConfig?.DisabledSkills, - Streaming = streaming, + Streaming = true }; } @@ -296,7 +296,7 @@ private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageDeltaEv { AgentId = this.Id, MessageId = deltaEvent.Data?.MessageId, - CreatedAt = DateTimeOffset.UtcNow + CreatedAt = deltaEvent.Timestamp }; } @@ -312,7 +312,7 @@ private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent a AgentId = this.Id, ResponseId = assistantMessage.Data?.MessageId, MessageId = assistantMessage.Data?.MessageId, - CreatedAt = DateTimeOffset.UtcNow + CreatedAt = assistantMessage.Timestamp }; } @@ -351,19 +351,19 @@ private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantUsageEvent usa if (usageEvent.Data.CacheWriteTokens is double cacheWriteTokens) { additionalCounts ??= []; - additionalCounts["CacheWriteTokens"] = (long)cacheWriteTokens; + additionalCounts[nameof(AssistantUsageData.CacheWriteTokens)] = (long)cacheWriteTokens; } if (usageEvent.Data.Cost is double cost) { additionalCounts ??= []; - additionalCounts["Cost"] = (long)cost; + additionalCounts[nameof(AssistantUsageData.Cost)] = (long)cost; } if (usageEvent.Data.Duration is double duration) { additionalCounts ??= []; - additionalCounts["Duration"] = (long)duration; + additionalCounts[nameof(AssistantUsageData.Duration)] = (long)duration; } return additionalCounts; From c9b4753d7fb40ee62fa3e36b45833d359faae356 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:43:41 -0800 Subject: [PATCH 33/34] Added integration tests --- dotnet/agent-framework-dotnet.slnx | 1 + .../GithubCopilotAgentTests.cs | 257 ++++++++++++++++++ ...s.AI.GithubCopilot.IntegrationTests.csproj | 12 + 3 files changed, 270 insertions(+) create mode 100644 dotnet/tests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests/GithubCopilotAgentTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests.csproj diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 4fcb30ae5c..2a9b81d44b 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -426,6 +426,7 @@ + diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests/GithubCopilotAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests/GithubCopilotAgentTests.cs new file mode 100644 index 0000000000..4d8d4ea3ed --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests/GithubCopilotAgentTests.cs @@ -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 ApproveAllAsync(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 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 = ApproveAllAsync, + }; + + 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 = ApproveAllAsync, + }; + + 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 = ApproveAllAsync, + McpServers = new Dictionary + { + ["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 = ApproveAllAsync, + McpServers = new Dictionary + { + ["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); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests.csproj new file mode 100644 index 0000000000..b51b09f284 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests.csproj @@ -0,0 +1,12 @@ + + + + + $(TargetFrameworksCore) + + + + + + + From 6ccde660e06342d92042ec33cd8a411aa7eb23ce Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:52:49 -0800 Subject: [PATCH 34/34] Small update --- .../GithubCopilotAgentTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests/GithubCopilotAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests/GithubCopilotAgentTests.cs index 4d8d4ea3ed..2ae499c479 100644 --- a/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests/GithubCopilotAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests/GithubCopilotAgentTests.cs @@ -13,7 +13,7 @@ public class GithubCopilotAgentTests { private const string SkipReason = "Integration tests require GitHub Copilot CLI installed. For local execution only."; - private static Task ApproveAllAsync(PermissionRequest request, PermissionInvocation invocation) + private static Task OnPermissionRequestAsync(PermissionRequest request, PermissionInvocation invocation) => Task.FromResult(new PermissionRequestResult { Kind = "approved" }); [Fact(Skip = SkipReason)] @@ -154,7 +154,7 @@ public async Task RunAsync_WithShellPermissions_ExecutesCommandAsync() SessionConfig sessionConfig = new() { - OnPermissionRequest = ApproveAllAsync, + OnPermissionRequest = OnPermissionRequestAsync, }; await using GithubCopilotAgent agent = new(client, sessionConfig); @@ -177,7 +177,7 @@ public async Task RunAsync_WithUrlPermissions_FetchesContentAsync() SessionConfig sessionConfig = new() { - OnPermissionRequest = ApproveAllAsync, + OnPermissionRequest = OnPermissionRequestAsync, }; await using GithubCopilotAgent agent = new(client, sessionConfig); @@ -200,7 +200,7 @@ public async Task RunAsync_WithLocalMcpServer_UsesServerToolsAsync() SessionConfig sessionConfig = new() { - OnPermissionRequest = ApproveAllAsync, + OnPermissionRequest = OnPermissionRequestAsync, McpServers = new Dictionary { ["filesystem"] = new McpLocalServerConfig @@ -233,7 +233,7 @@ public async Task RunAsync_WithRemoteMcpServer_UsesServerToolsAsync() SessionConfig sessionConfig = new() { - OnPermissionRequest = ApproveAllAsync, + OnPermissionRequest = OnPermissionRequestAsync, McpServers = new Dictionary { ["microsoft-learn"] = new McpRemoteServerConfig