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

* Initial plan

* Add GitHub Copilot SDK AIAgent implementation with tests

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

* Add projects to solution and fix sample imports

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

* Improve pragma comment clarity in GithubCopilotAgentThread

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

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

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

* Add ownsClient parameter to allow caller control over client disposal

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

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

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

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

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

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

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

* Fix test file encoding and remove redundant cast

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

* Add AsAIAgent extension methods for CopilotClient with tests

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

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

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

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

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

* Revert session persistence changes - sessions dispose after each call

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

* Add CreatedAt property mapping using DateTimeOffset.UtcNow

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

* Add DataContent handling via temp files and attachments

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

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

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

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

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

* Updated sample and session config mapping

* Added instructions parameter

* Updated README

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

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

* Remove StreamJsonRpc reference from sample project

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

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

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

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

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

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

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

* Resolved build errors

* Addressed comments

* Small fix

* Addressed comment

* Small fix

* Addressed comments

* Added integration tests

* Small update

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
This commit is contained in:
Copilot
2026-01-27 18:51:56 +00:00
committed by GitHub
co-authored by westey-m stephentoub copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Dmytro Struk
parent 968621e817
commit e82d9f5e45
16 changed files with 1300 additions and 0 deletions
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.GithubCopilot;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace GitHub.Copilot.SDK;
/// <summary>
/// Provides extension methods for <see cref="CopilotClient"/>
/// to simplify the creation of GitHub Copilot agents.
/// </summary>
/// <remarks>
/// These extensions bridge the gap between GitHub Copilot SDK client objects
/// and the Microsoft Agent Framework.
/// <para>
/// They allow developers to easily create AI agents that can interact
/// with GitHub Copilot by handling the conversion from Copilot clients to
/// <see cref="GithubCopilotAgent"/> instances that implement the <see cref="AIAgent"/> interface.
/// </para>
/// </remarks>
public static class CopilotClientExtensions
{
/// <summary>
/// Retrieves an instance of <see cref="AIAgent"/> for a GitHub Copilot client.
/// </summary>
/// <param name="client">The <see cref="CopilotClient"/> to use for the agent.</param>
/// <param name="sessionConfig">Optional session configuration for the agent.</param>
/// <param name="ownsClient">Whether the agent owns the client and should dispose it. Default is false.</param>
/// <param name="id">The unique identifier for the agent.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="description">The description of the agent.</param>
/// <returns>An <see cref="AIAgent"/> instance backed by the GitHub Copilot client.</returns>
public static AIAgent AsAIAgent(
this CopilotClient client,
SessionConfig? sessionConfig = null,
bool ownsClient = false,
string? id = null,
string? name = null,
string? description = null)
{
Throw.IfNull(client);
return new GithubCopilotAgent(client, sessionConfig, ownsClient, id, name, description);
}
/// <summary>
/// Retrieves an instance of <see cref="AIAgent"/> for a GitHub Copilot client.
/// </summary>
/// <param name="client">The <see cref="CopilotClient"/> to use for the agent.</param>
/// <param name="ownsClient">Whether the agent owns the client and should dispose it. Default is false.</param>
/// <param name="id">The unique identifier for the agent.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="description">The description of the agent.</param>
/// <param name="tools">The tools to make available to the agent.</param>
/// <param name="instructions">Optional instructions to append as a system message.</param>
/// <returns>An <see cref="AIAgent"/> instance backed by the GitHub Copilot client.</returns>
public static AIAgent AsAIAgent(
this CopilotClient client,
bool ownsClient = false,
string? id = null,
string? name = null,
string? description = null,
IList<AITool>? tools = null,
string? instructions = null)
{
Throw.IfNull(client);
return new GithubCopilotAgent(client, ownsClient, id, name, description, tools, instructions);
}
}
@@ -0,0 +1,470 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using GitHub.Copilot.SDK;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.GithubCopilot;
/// <summary>
/// Represents an <see cref="AIAgent"/> that uses the GitHub Copilot SDK to provide agentic capabilities.
/// </summary>
public sealed class GithubCopilotAgent : AIAgent, IAsyncDisposable
{
private const string DefaultName = "GitHub Copilot Agent";
private const string DefaultDescription = "An AI agent powered by GitHub Copilot";
private readonly CopilotClient _copilotClient;
private readonly string? _id;
private readonly string _name;
private readonly string _description;
private readonly SessionConfig? _sessionConfig;
private readonly bool _ownsClient;
/// <summary>
/// Initializes a new instance of the <see cref="GithubCopilotAgent"/> class.
/// </summary>
/// <param name="copilotClient">The Copilot client to use for interacting with GitHub Copilot.</param>
/// <param name="sessionConfig">Optional session configuration for the agent.</param>
/// <param name="ownsClient">Whether the agent owns the client and should dispose it. Default is false.</param>
/// <param name="id">The unique identifier for the agent.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="description">The description of the agent.</param>
public GithubCopilotAgent(
CopilotClient copilotClient,
SessionConfig? sessionConfig = null,
bool ownsClient = false,
string? id = null,
string? name = null,
string? description = null)
{
_ = Throw.IfNull(copilotClient);
this._copilotClient = copilotClient;
this._sessionConfig = sessionConfig;
this._ownsClient = ownsClient;
this._id = id;
this._name = name ?? DefaultName;
this._description = description ?? DefaultDescription;
}
/// <summary>
/// Initializes a new instance of the <see cref="GithubCopilotAgent"/> class.
/// </summary>
/// <param name="copilotClient">The Copilot client to use for interacting with GitHub Copilot.</param>
/// <param name="ownsClient">Whether the agent owns the client and should dispose it. Default is false.</param>
/// <param name="id">The unique identifier for the agent.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="description">The description of the agent.</param>
/// <param name="tools">The tools to make available to the agent.</param>
/// <param name="instructions">Optional instructions to append as a system message.</param>
public GithubCopilotAgent(
CopilotClient copilotClient,
bool ownsClient = false,
string? id = null,
string? name = null,
string? description = null,
IList<AITool>? tools = null,
string? instructions = null)
: this(
copilotClient,
GetSessionConfig(tools, instructions),
ownsClient,
id,
name,
description)
{
}
/// <inheritdoc/>
public sealed override ValueTask<AgentSession> GetNewSessionAsync(CancellationToken cancellationToken = default)
=> new(new GithubCopilotAgentSession());
/// <summary>
/// Get a new <see cref="AgentSession"/> instance using an existing session id, to continue that conversation.
/// </summary>
/// <param name="sessionId">The session id to continue.</param>
/// <returns>A new <see cref="AgentSession"/> instance.</returns>
public ValueTask<AgentSession> GetNewSessionAsync(string sessionId)
=> new(new GithubCopilotAgentSession() { SessionId = sessionId });
/// <inheritdoc/>
public override ValueTask<AgentSession> DeserializeSessionAsync(
JsonElement serializedSession,
JsonSerializerOptions? jsonSerializerOptions = null,
CancellationToken cancellationToken = default)
=> new(new GithubCopilotAgentSession(serializedSession, jsonSerializerOptions));
/// <inheritdoc/>
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
=> this.RunCoreStreamingAsync(messages, session, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
/// <inheritdoc/>
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(messages);
// Ensure we have a valid session
session ??= await this.GetNewSessionAsync(cancellationToken).ConfigureAwait(false);
if (session is not GithubCopilotAgentSession typedSession)
{
throw new InvalidOperationException(
$"The provided session type {session.GetType()} is not compatible with the agent. Only GitHub Copilot agent created sessions are supported.");
}
// Ensure the client is started
await this.EnsureClientStartedAsync(cancellationToken).ConfigureAwait(false);
// Create or resume a session with streaming enabled
SessionConfig sessionConfig = this._sessionConfig != null
? new SessionConfig
{
Model = this._sessionConfig.Model,
Tools = this._sessionConfig.Tools,
SystemMessage = this._sessionConfig.SystemMessage,
AvailableTools = this._sessionConfig.AvailableTools,
ExcludedTools = this._sessionConfig.ExcludedTools,
Provider = this._sessionConfig.Provider,
OnPermissionRequest = this._sessionConfig.OnPermissionRequest,
McpServers = this._sessionConfig.McpServers,
CustomAgents = this._sessionConfig.CustomAgents,
SkillDirectories = this._sessionConfig.SkillDirectories,
DisabledSkills = this._sessionConfig.DisabledSkills,
Streaming = true
}
: new SessionConfig { Streaming = true };
CopilotSession copilotSession;
if (typedSession.SessionId is not null)
{
copilotSession = await this._copilotClient.ResumeSessionAsync(
typedSession.SessionId,
this.CreateResumeConfig(),
cancellationToken).ConfigureAwait(false);
}
else
{
copilotSession = await this._copilotClient.CreateSessionAsync(sessionConfig, cancellationToken).ConfigureAwait(false);
typedSession.SessionId = copilotSession.SessionId;
}
try
{
Channel<AgentResponseUpdate> channel = Channel.CreateUnbounded<AgentResponseUpdate>();
// Subscribe to session events
using IDisposable subscription = copilotSession.On(evt =>
{
switch (evt)
{
case AssistantMessageDeltaEvent deltaEvent:
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(deltaEvent));
break;
case AssistantMessageEvent assistantMessage:
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(assistantMessage));
break;
case AssistantUsageEvent usageEvent:
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(usageEvent));
break;
case SessionIdleEvent idleEvent:
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(idleEvent));
channel.Writer.TryComplete();
break;
case SessionErrorEvent errorEvent:
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(errorEvent));
channel.Writer.TryComplete(new InvalidOperationException(
$"Session error: {errorEvent.Data?.Message ?? "Unknown error"}"));
break;
default:
// Handle all other event types by storing as RawRepresentation
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(evt));
break;
}
});
List<string> tempFiles = [];
try
{
// Build prompt from text content
string prompt = string.Join("\n", messages.Select(m => m.Text));
// Handle DataContent as attachments
List<UserMessageDataAttachmentsItem>? attachments = await ProcessDataContentAttachmentsAsync(
messages,
tempFiles,
cancellationToken).ConfigureAwait(false);
// Send the message with attachments
MessageOptions messageOptions = new() { Prompt = prompt };
if (attachments is not null)
{
messageOptions.Attachments = [.. attachments];
}
await copilotSession.SendAsync(messageOptions, cancellationToken).ConfigureAwait(false);
// Yield updates as they arrive
await foreach (AgentResponseUpdate update in channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
{
yield return update;
}
}
finally
{
CleanupTempFiles(tempFiles);
}
}
finally
{
await copilotSession.DisposeAsync().ConfigureAwait(false);
}
}
/// <inheritdoc/>
protected override string? IdCore => this._id;
/// <inheritdoc/>
public override string Name => this._name;
/// <inheritdoc/>
public override string Description => this._description;
/// <summary>
/// Disposes the agent and releases resources.
/// </summary>
/// <returns>A value task representing the asynchronous dispose operation.</returns>
public async ValueTask DisposeAsync()
{
if (this._ownsClient)
{
await this._copilotClient.DisposeAsync().ConfigureAwait(false);
}
}
private async Task EnsureClientStartedAsync(CancellationToken cancellationToken)
{
if (this._copilotClient.State != ConnectionState.Connected)
{
await this._copilotClient.StartAsync(cancellationToken).ConfigureAwait(false);
}
}
private ResumeSessionConfig CreateResumeConfig()
{
return new ResumeSessionConfig
{
Tools = this._sessionConfig?.Tools,
Provider = this._sessionConfig?.Provider,
OnPermissionRequest = this._sessionConfig?.OnPermissionRequest,
McpServers = this._sessionConfig?.McpServers,
CustomAgents = this._sessionConfig?.CustomAgents,
SkillDirectories = this._sessionConfig?.SkillDirectories,
DisabledSkills = this._sessionConfig?.DisabledSkills,
Streaming = true
};
}
private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageDeltaEvent deltaEvent)
{
TextContent textContent = new(deltaEvent.Data?.DeltaContent ?? string.Empty)
{
RawRepresentation = deltaEvent
};
return new AgentResponseUpdate(ChatRole.Assistant, [textContent])
{
AgentId = this.Id,
MessageId = deltaEvent.Data?.MessageId,
CreatedAt = deltaEvent.Timestamp
};
}
private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent assistantMessage)
{
TextContent textContent = new(assistantMessage.Data?.Content ?? string.Empty)
{
RawRepresentation = assistantMessage
};
return new AgentResponseUpdate(ChatRole.Assistant, [textContent])
{
AgentId = this.Id,
ResponseId = assistantMessage.Data?.MessageId,
MessageId = assistantMessage.Data?.MessageId,
CreatedAt = assistantMessage.Timestamp
};
}
private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantUsageEvent usageEvent)
{
UsageDetails usageDetails = new()
{
InputTokenCount = (int?)(usageEvent.Data?.InputTokens),
OutputTokenCount = (int?)(usageEvent.Data?.OutputTokens),
TotalTokenCount = (int?)((usageEvent.Data?.InputTokens ?? 0) + (usageEvent.Data?.OutputTokens ?? 0)),
CachedInputTokenCount = (int?)(usageEvent.Data?.CacheReadTokens),
AdditionalCounts = GetAdditionalCounts(usageEvent),
};
UsageContent usageContent = new(usageDetails)
{
RawRepresentation = usageEvent
};
return new AgentResponseUpdate(ChatRole.Assistant, [usageContent])
{
AgentId = this.Id,
CreatedAt = usageEvent.Timestamp
};
}
private static AdditionalPropertiesDictionary<long>? GetAdditionalCounts(AssistantUsageEvent usageEvent)
{
if (usageEvent.Data is null)
{
return null;
}
AdditionalPropertiesDictionary<long>? additionalCounts = null;
if (usageEvent.Data.CacheWriteTokens is double cacheWriteTokens)
{
additionalCounts ??= [];
additionalCounts[nameof(AssistantUsageData.CacheWriteTokens)] = (long)cacheWriteTokens;
}
if (usageEvent.Data.Cost is double cost)
{
additionalCounts ??= [];
additionalCounts[nameof(AssistantUsageData.Cost)] = (long)cost;
}
if (usageEvent.Data.Duration is double duration)
{
additionalCounts ??= [];
additionalCounts[nameof(AssistantUsageData.Duration)] = (long)duration;
}
return additionalCounts;
}
private AgentResponseUpdate ConvertToAgentResponseUpdate(SessionEvent sessionEvent)
{
// Handle arbitrary events by storing as RawRepresentation
AIContent content = new()
{
RawRepresentation = sessionEvent
};
return new AgentResponseUpdate(ChatRole.Assistant, [content])
{
AgentId = this.Id,
CreatedAt = sessionEvent.Timestamp
};
}
private static SessionConfig? GetSessionConfig(IList<AITool>? tools, string? instructions)
{
List<AIFunction>? mappedTools = tools is { Count: > 0 } ? tools.OfType<AIFunction>().ToList() : null;
SystemMessageConfig? systemMessage = instructions is not null ? new SystemMessageConfig { Mode = SystemMessageMode.Append, Content = instructions } : null;
if (mappedTools is null && systemMessage is null)
{
return null;
}
return new SessionConfig { Tools = mappedTools, SystemMessage = systemMessage };
}
private static readonly Dictionary<string, string> s_mediaTypeExtensions = new(StringComparer.OrdinalIgnoreCase)
{
["image/png"] = ".png",
["image/jpeg"] = ".jpg",
["image/jpg"] = ".jpg",
["image/gif"] = ".gif",
["image/webp"] = ".webp",
["image/svg+xml"] = ".svg",
["text/plain"] = ".txt",
["text/html"] = ".html",
["text/markdown"] = ".md",
["application/json"] = ".json",
["application/xml"] = ".xml",
["application/pdf"] = ".pdf"
};
private static string GetExtensionForMediaType(string? mediaType)
{
return mediaType is not null && s_mediaTypeExtensions.TryGetValue(mediaType, out string? extension) ? extension : ".dat";
}
private static async Task<List<UserMessageDataAttachmentsItem>?> ProcessDataContentAttachmentsAsync(
IEnumerable<ChatMessage> messages,
List<string> tempFiles,
CancellationToken cancellationToken)
{
List<UserMessageDataAttachmentsItem>? attachments = null;
foreach (ChatMessage message in messages)
{
foreach (AIContent content in message.Contents)
{
if (content is DataContent dataContent)
{
// Write DataContent to a temp file
string tempFilePath = Path.Combine(Path.GetTempPath(), $"agentframework_copilot_data_{Guid.NewGuid()}{GetExtensionForMediaType(dataContent.MediaType)}");
await File.WriteAllBytesAsync(tempFilePath, dataContent.Data.ToArray(), cancellationToken).ConfigureAwait(false);
tempFiles.Add(tempFilePath);
// Create attachment
attachments ??= [];
attachments.Add(new UserMessageDataAttachmentsItem
{
Type = UserMessageDataAttachmentsItemType.File,
Path = tempFilePath,
DisplayName = Path.GetFileName(tempFilePath)
});
}
}
}
return attachments;
}
private static void CleanupTempFiles(List<string> tempFiles)
{
foreach (string tempFile in tempFiles)
{
try
{
if (File.Exists(tempFile))
{
File.Delete(tempFile);
}
}
catch
{
// Best effort cleanup
}
}
}
}
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
namespace Microsoft.Agents.AI.GithubCopilot;
/// <summary>
/// Represents a session for a GitHub Copilot agent conversation.
/// </summary>
public sealed class GithubCopilotAgentSession : AgentSession
{
/// <summary>
/// Gets or sets the session ID for the GitHub Copilot conversation.
/// </summary>
public string? SessionId { get; internal set; }
/// <summary>
/// Initializes a new instance of the <see cref="GithubCopilotAgentSession"/> class.
/// </summary>
internal GithubCopilotAgentSession()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="GithubCopilotAgentSession"/> class from serialized data.
/// </summary>
/// <param name="serializedThread">The serialized thread data.</param>
/// <param name="jsonSerializerOptions">Optional JSON serialization options.</param>
internal GithubCopilotAgentSession(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
{
// The JSON serialization uses camelCase
if (serializedThread.TryGetProperty("sessionId", out JsonElement sessionIdElement))
{
this.SessionId = sessionIdElement.GetString();
}
}
/// <inheritdoc/>
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
State state = new()
{
SessionId = this.SessionId
};
return JsonSerializer.SerializeToElement(
state,
GithubCopilotJsonUtilities.DefaultOptions.GetTypeInfo(typeof(State)));
}
internal sealed class State
{
public string? SessionId { get; set; }
}
}
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI.GithubCopilot;
/// <summary>
/// Provides utility methods and configurations for JSON serialization operations within the GitHub Copilot agent implementation.
/// </summary>
internal static partial class GithubCopilotJsonUtilities
{
/// <summary>
/// Gets the default <see cref="JsonSerializerOptions"/> instance used for JSON serialization operations.
/// </summary>
public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions();
/// <summary>
/// Creates and configures the default JSON serialization options.
/// </summary>
/// <returns>The configured options.</returns>
private static JsonSerializerOptions CreateDefaultOptions()
{
// Copy the configuration from the source generated context.
JsonSerializerOptions options = new(JsonContext.Default.Options)
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
// Chain in the resolvers from both AgentAbstractionsJsonUtilities and our source generated context.
options.TypeInfoResolverChain.Clear();
options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!);
options.TypeInfoResolverChain.Add(JsonContext.Default.Options.TypeInfoResolver!);
options.MakeReadOnly();
return options;
}
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
UseStringEnumConverter = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString)]
[JsonSerializable(typeof(GithubCopilotAgentSession.State))]
[ExcludeFromCodeCoverage]
private sealed partial class JsonContext : JsonSerializerContext;
}
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<VersionSuffix>preview</VersionSuffix>
<!-- GitHub.Copilot.SDK only supports .NET 8.0+ -->
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
<InjectDiagnosticClassesOnLegacy>true</InjectDiagnosticClassesOnLegacy>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<ItemGroup>
<ProjectReference Include="..\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GitHub.Copilot.SDK" />
</ItemGroup>
<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Microsoft Agent Framework GitHub Copilot</Title>
<Description>Provides Microsoft Agent Framework support for GitHub Copilot SDK.</Description>
</PropertyGroup>
</Project>