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