chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.GitHub.Copilot;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace GitHub.Copilot;
|
||||
|
||||
/// <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<AIFunctionDeclaration>? tools = null,
|
||||
string? instructions = null)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
return new GitHubCopilotAgent(client, ownsClient, id, name, description, tools, instructions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,673 @@
|
||||
// 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.Text.Json.Serialization.Metadata;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Copilot;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.GitHub.Copilot;
|
||||
|
||||
/// <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;
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <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>
|
||||
/// <param name="jsonSerializerOptions">Optional JSON serializer options. Defaults to <see cref="GitHubCopilotJsonUtilities.DefaultOptions"/>.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory used to create the agent's logger.</param>
|
||||
/// <remarks>
|
||||
/// When a tool wrapped in <see cref="ApprovalRequiredAIFunction"/> is registered and the supplied
|
||||
/// <paramref name="sessionConfig"/> does not already define a <c>Hooks.OnPreToolUse</c> handler, the agent installs a
|
||||
/// default <c>OnPreToolUse</c> hook that returns <c>"ask"</c> for those tools (routing the decision to
|
||||
/// <c>SessionConfig.OnPermissionRequest</c>) and defers all other tools. If the caller supplies their own
|
||||
/// <c>OnPreToolUse</c> hook, it takes precedence and the caller is fully responsible for approval handling; in that
|
||||
/// case a warning is logged for any approval-required tool that will not be automatically gated.
|
||||
/// </remarks>
|
||||
public GitHubCopilotAgent(
|
||||
CopilotClient copilotClient,
|
||||
SessionConfig? sessionConfig = null,
|
||||
bool ownsClient = false,
|
||||
string? id = null,
|
||||
string? name = null,
|
||||
string? description = null,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
_ = Throw.IfNull(copilotClient);
|
||||
|
||||
this._copilotClient = copilotClient;
|
||||
this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<GitHubCopilotAgent>();
|
||||
this._sessionConfig = ConfigureApprovalHook(sessionConfig, this._logger);
|
||||
this._ownsClient = ownsClient;
|
||||
this._id = id;
|
||||
this._name = name ?? DefaultName;
|
||||
this._description = description ?? DefaultDescription;
|
||||
this._jsonSerializerOptions = jsonSerializerOptions ?? GitHubCopilotJsonUtilities.DefaultOptions;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="jsonSerializerOptions">Optional JSON serializer options. Defaults to <see cref="GitHubCopilotJsonUtilities.DefaultOptions"/>.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory used to create the agent's logger.</param>
|
||||
/// <remarks>
|
||||
/// When a tool wrapped in <see cref="ApprovalRequiredAIFunction"/> is registered, the agent installs a default
|
||||
/// <c>Hooks.OnPreToolUse</c> handler that returns <c>"ask"</c> for those tools (routing the decision to
|
||||
/// <c>SessionConfig.OnPermissionRequest</c>) and defers all other tools.
|
||||
/// </remarks>
|
||||
public GitHubCopilotAgent(
|
||||
CopilotClient copilotClient,
|
||||
bool ownsClient = false,
|
||||
string? id = null,
|
||||
string? name = null,
|
||||
string? description = null,
|
||||
IList<AIFunctionDeclaration>? tools = null,
|
||||
string? instructions = null,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
: this(
|
||||
copilotClient,
|
||||
GetSessionConfig(tools, instructions),
|
||||
ownsClient,
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
jsonSerializerOptions,
|
||||
loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected sealed override ValueTask<AgentSession> CreateSessionCoreAsync(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> CreateSessionAsync(string sessionId)
|
||||
=> new(new GitHubCopilotAgentSession() { SessionId = sessionId });
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ValueTask<JsonElement> SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNull(session);
|
||||
|
||||
if (session is not GitHubCopilotAgentSession typedSession)
|
||||
{
|
||||
throw new InvalidOperationException($"The provided session type '{session.GetType().Name}' is not compatible with this agent. Only sessions of type '{nameof(GitHubCopilotAgentSession)}' can be serialized by this agent.");
|
||||
}
|
||||
|
||||
return new(typedSession.Serialize(jsonSerializerOptions));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(
|
||||
JsonElement serializedState,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> new(GitHubCopilotAgentSession.Deserialize(serializedState, 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.CreateSessionAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (session is not GitHubCopilotAgentSession typedSession)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The provided session type '{session.GetType().Name}' is not compatible with this agent. Only sessions of type '{nameof(GitHubCopilotAgentSession)}' can be used by this agent.");
|
||||
}
|
||||
|
||||
// Ensure the client is started
|
||||
await this.EnsureClientStartedAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Create or resume a session with streaming enabled by default
|
||||
SessionConfig sessionConfig = this._sessionConfig != null
|
||||
? CopySessionConfig(this._sessionConfig)
|
||||
: new SessionConfig { Streaming = true };
|
||||
|
||||
bool isStreaming = 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<SessionEvent>(evt =>
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case AssistantMessageDeltaEvent deltaEvent:
|
||||
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(deltaEvent));
|
||||
break;
|
||||
|
||||
case AssistantMessageEvent assistantMessage:
|
||||
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(assistantMessage, isStreaming));
|
||||
break;
|
||||
|
||||
case ToolExecutionStartEvent toolStart:
|
||||
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(toolStart));
|
||||
break;
|
||||
|
||||
case ToolExecutionCompleteEvent toolComplete:
|
||||
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(toolComplete));
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
string? tempDir = null;
|
||||
try
|
||||
{
|
||||
// Build prompt from text content
|
||||
string prompt = string.Join("\n", messages.Select(m => m.Text));
|
||||
|
||||
// Handle DataContent as attachments
|
||||
(List<AttachmentFile>? attachments, tempDir) = await ProcessDataContentAttachmentsAsync(
|
||||
messages,
|
||||
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
|
||||
{
|
||||
CleanupTempDir(tempDir);
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
await this._copilotClient.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private ResumeSessionConfig CreateResumeConfig()
|
||||
{
|
||||
return CopyResumeSessionConfig(this._sessionConfig);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies all supported properties from a source <see cref="SessionConfig"/> into a new instance,
|
||||
/// preserving <see cref="SessionConfigBase.Streaming"/> from the source (defaulting to <c>true</c> if unset).
|
||||
/// </summary>
|
||||
internal static SessionConfig CopySessionConfig(SessionConfig source)
|
||||
{
|
||||
SessionConfig copy = source.Clone();
|
||||
copy.Streaming = source.Streaming ?? true;
|
||||
return copy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies all supported properties from a source <see cref="SessionConfig"/> into a new
|
||||
/// <see cref="ResumeSessionConfig"/>, preserving <see cref="SessionConfigBase.Streaming"/>
|
||||
/// from the source (defaulting to <c>true</c> if unset).
|
||||
/// </summary>
|
||||
internal static ResumeSessionConfig CopyResumeSessionConfig(SessionConfig? source)
|
||||
{
|
||||
return new ResumeSessionConfig
|
||||
{
|
||||
Model = source?.Model,
|
||||
ReasoningEffort = source?.ReasoningEffort,
|
||||
Tools = source?.Tools,
|
||||
SystemMessage = source?.SystemMessage,
|
||||
AvailableTools = source?.AvailableTools,
|
||||
ExcludedTools = source?.ExcludedTools,
|
||||
Provider = source?.Provider,
|
||||
OnPermissionRequest = source?.OnPermissionRequest,
|
||||
OnUserInputRequest = source?.OnUserInputRequest,
|
||||
Hooks = source?.Hooks,
|
||||
WorkingDirectory = source?.WorkingDirectory,
|
||||
ConfigDirectory = source?.ConfigDirectory,
|
||||
McpServers = source?.McpServers,
|
||||
CustomAgents = source?.CustomAgents,
|
||||
SkillDirectories = source?.SkillDirectories,
|
||||
DisabledSkills = source?.DisabledSkills,
|
||||
InfiniteSessions = source?.InfiniteSessions,
|
||||
Streaming = source?.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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an <see cref="AssistantMessageEvent"/> to an <see cref="AgentResponseUpdate"/>.
|
||||
/// When streaming is enabled, text was already delivered via delta events, so only raw metadata is emitted.
|
||||
/// When streaming is disabled, the full message text is emitted as <see cref="TextContent"/>.
|
||||
/// </summary>
|
||||
internal AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent assistantMessage, bool isStreaming)
|
||||
{
|
||||
// When streaming, text was already delivered via AssistantMessageDeltaEvent.
|
||||
// When not streaming, this is the only opportunity to emit the response text.
|
||||
AIContent content = isStreaming
|
||||
? new AIContent { RawRepresentation = assistantMessage }
|
||||
: new TextContent(assistantMessage.Data?.Content ?? string.Empty) { RawRepresentation = assistantMessage };
|
||||
|
||||
return new AgentResponseUpdate(ChatRole.Assistant, [content])
|
||||
{
|
||||
AgentId = this.Id,
|
||||
ResponseId = assistantMessage.Data?.MessageId,
|
||||
MessageId = assistantMessage.Data?.MessageId,
|
||||
CreatedAt = assistantMessage.Timestamp
|
||||
};
|
||||
}
|
||||
|
||||
internal AgentResponseUpdate ConvertToAgentResponseUpdate(ToolExecutionStartEvent toolStart)
|
||||
{
|
||||
IDictionary<string, object?>? arguments = this.ParseArguments(toolStart.Data?.Arguments);
|
||||
|
||||
FunctionCallContent content = new(
|
||||
toolStart.Data?.ToolCallId ?? string.Empty,
|
||||
toolStart.Data?.ToolName ?? string.Empty,
|
||||
arguments)
|
||||
{
|
||||
RawRepresentation = toolStart
|
||||
};
|
||||
|
||||
return new AgentResponseUpdate(ChatRole.Assistant, [content])
|
||||
{
|
||||
AgentId = this.Id,
|
||||
CreatedAt = toolStart.Timestamp
|
||||
};
|
||||
}
|
||||
|
||||
internal AgentResponseUpdate ConvertToAgentResponseUpdate(ToolExecutionCompleteEvent toolComplete)
|
||||
{
|
||||
object? result = toolComplete.Data?.Success == true
|
||||
? toolComplete.Data?.Result?.Content
|
||||
: toolComplete.Data?.Error?.Message ?? "Tool execution failed";
|
||||
|
||||
FunctionResultContent content = new(
|
||||
toolComplete.Data?.ToolCallId ?? string.Empty,
|
||||
result)
|
||||
{
|
||||
RawRepresentation = toolComplete
|
||||
};
|
||||
|
||||
return new AgentResponseUpdate(ChatRole.Tool, [content])
|
||||
{
|
||||
AgentId = this.Id,
|
||||
CreatedAt = toolComplete.Timestamp
|
||||
};
|
||||
}
|
||||
|
||||
private IDictionary<string, object?>? ParseArguments(object? arguments)
|
||||
{
|
||||
if (arguments is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (arguments is JsonElement jsonElement)
|
||||
{
|
||||
if (jsonElement.ValueKind == JsonValueKind.Null || jsonElement.ValueKind == JsonValueKind.Undefined)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var typeInfo = (JsonTypeInfo<Dictionary<string, object?>>)this._jsonSerializerOptions.GetTypeInfo(typeof(Dictionary<string, object?>));
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize(jsonElement.GetRawText(), typeInfo);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return new Dictionary<string, object?> { ["value"] = jsonElement.ToString() };
|
||||
}
|
||||
}
|
||||
|
||||
if (arguments is IDictionary<string, object?> dict)
|
||||
{
|
||||
return dict;
|
||||
}
|
||||
|
||||
return new Dictionary<string, object?> { ["value"] = arguments.ToString() };
|
||||
}
|
||||
|
||||
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 long cacheWriteTokens)
|
||||
{
|
||||
additionalCounts ??= [];
|
||||
additionalCounts[nameof(AssistantUsageData.CacheWriteTokens)] = cacheWriteTokens;
|
||||
}
|
||||
|
||||
if (usageEvent.Data.Cost is double cost)
|
||||
{
|
||||
additionalCounts ??= [];
|
||||
additionalCounts[nameof(AssistantUsageData.Cost)] = (long)cost;
|
||||
}
|
||||
|
||||
if (usageEvent.Data.Duration is TimeSpan duration)
|
||||
{
|
||||
additionalCounts ??= [];
|
||||
additionalCounts[nameof(AssistantUsageData.Duration)] = (long)duration.TotalMilliseconds;
|
||||
}
|
||||
|
||||
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<AIFunctionDeclaration>? tools, string? instructions)
|
||||
{
|
||||
List<AIFunctionDeclaration>? mappedTools = tools is { Count: > 0 } ? tools.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 };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs a default <c>OnPreToolUse</c> hook that gates tools wrapped in <see cref="ApprovalRequiredAIFunction"/>
|
||||
/// by returning <c>"ask"</c> (routing the decision to <c>SessionConfig.OnPermissionRequest</c>) while deferring all
|
||||
/// other tools, so the GitHub Copilot SDK enforces approval through its native pre-tool-use hook.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The source <paramref name="sessionConfig"/> is returned unchanged when it contains no approval-required tools.
|
||||
/// If the caller already supplied a <c>Hooks.OnPreToolUse</c> handler, it takes precedence and is left untouched; a
|
||||
/// warning is logged for any approval-required tool that will therefore not be automatically gated. Otherwise a
|
||||
/// clone is returned (with a fresh <see cref="SessionHooks"/>) so the caller-supplied configuration is not mutated.
|
||||
/// </remarks>
|
||||
private static SessionConfig? ConfigureApprovalHook(SessionConfig? sessionConfig, ILogger logger)
|
||||
{
|
||||
if (sessionConfig?.Tools is not { Count: > 0 } tools)
|
||||
{
|
||||
return sessionConfig;
|
||||
}
|
||||
|
||||
HashSet<string> approvalRequiredToolNames = new(StringComparer.Ordinal);
|
||||
foreach (AIFunctionDeclaration tool in tools)
|
||||
{
|
||||
if (tool is AIFunction function && function.GetService<ApprovalRequiredAIFunction>() is not null)
|
||||
{
|
||||
approvalRequiredToolNames.Add(function.Name);
|
||||
}
|
||||
}
|
||||
|
||||
if (approvalRequiredToolNames.Count == 0)
|
||||
{
|
||||
return sessionConfig;
|
||||
}
|
||||
|
||||
// A caller-supplied OnPreToolUse hook takes precedence and is fully responsible for approval handling.
|
||||
// Warn so the developer knows the ApprovalRequiredAIFunction marker(s) will not be automatically gated.
|
||||
if (sessionConfig.Hooks?.OnPreToolUse is not null)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Warning))
|
||||
{
|
||||
logger.LogApprovalGatingSkippedDueToCustomHook(
|
||||
approvalRequiredToolNames.Count,
|
||||
string.Join(", ", approvalRequiredToolNames));
|
||||
}
|
||||
return sessionConfig;
|
||||
}
|
||||
|
||||
SessionConfig configured = sessionConfig.Clone();
|
||||
|
||||
// SessionConfig.Clone() shallow-copies Hooks, so build a fresh SessionHooks (preserving any other hooks)
|
||||
// to avoid mutating the caller's instance when setting OnPreToolUse.
|
||||
SessionHooks hooks = CloneHooks(configured.Hooks);
|
||||
hooks.OnPreToolUse = (input, invocation) =>
|
||||
Task.FromResult(
|
||||
approvalRequiredToolNames.Contains(input.ToolName)
|
||||
? new PreToolUseHookOutput
|
||||
{
|
||||
PermissionDecision = "ask",
|
||||
PermissionDecisionReason = $"Tool '{input.ToolName}' is marked as requiring approval (ApprovalRequiredAIFunction).",
|
||||
}
|
||||
: null);
|
||||
configured.Hooks = hooks;
|
||||
|
||||
return configured;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a shallow copy of a <see cref="SessionHooks"/> instance, preserving all configured hook delegates.
|
||||
/// </summary>
|
||||
private static SessionHooks CloneHooks(SessionHooks? source)
|
||||
{
|
||||
SessionHooks clone = new();
|
||||
if (source is not null)
|
||||
{
|
||||
clone.OnPreToolUse = source.OnPreToolUse;
|
||||
clone.OnPreMcpToolCall = source.OnPreMcpToolCall;
|
||||
clone.OnPostToolUse = source.OnPostToolUse;
|
||||
clone.OnPostToolUseFailure = source.OnPostToolUseFailure;
|
||||
clone.OnUserPromptSubmitted = source.OnUserPromptSubmitted;
|
||||
clone.OnSessionStart = source.OnSessionStart;
|
||||
clone.OnSessionEnd = source.OnSessionEnd;
|
||||
clone.OnErrorOccurred = source.OnErrorOccurred;
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
private static async Task<(List<AttachmentFile>? Attachments, string? TempDir)> ProcessDataContentAttachmentsAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<AttachmentFile>? attachments = null;
|
||||
string? tempDir = null;
|
||||
foreach (ChatMessage message in messages)
|
||||
{
|
||||
foreach (AIContent content in message.Contents)
|
||||
{
|
||||
if (content is DataContent dataContent)
|
||||
{
|
||||
tempDir ??= Directory.CreateDirectory(
|
||||
Path.Combine(Path.GetTempPath(), $"af_copilot_{Guid.NewGuid():N}")).FullName;
|
||||
|
||||
string tempFilePath = await dataContent.SaveToAsync(tempDir, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
attachments ??= [];
|
||||
attachments.Add(new AttachmentFile
|
||||
{
|
||||
Path = tempFilePath,
|
||||
DisplayName = Path.GetFileName(tempFilePath)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (attachments, tempDir);
|
||||
}
|
||||
|
||||
private static void CleanupTempDir(string? tempDir)
|
||||
{
|
||||
if (tempDir is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best effort cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Agents.AI.GitHub.Copilot;
|
||||
#pragma warning disable SYSLIB1006 // Multiple logging methods cannot use the same event id within a class
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for logging <see cref="GitHubCopilotAgent"/> invocations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This extension uses the <see cref="LoggerMessageAttribute"/> to
|
||||
/// generate logging code at compile time to achieve optimized code.
|
||||
/// </remarks>
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal static partial class GitHubCopilotAgentLogMessages
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs a warning when a custom <c>OnPreToolUse</c> hook is present and approval-gating will not be applied automatically.
|
||||
/// </summary>
|
||||
[LoggerMessage(
|
||||
Level = LogLevel.Warning,
|
||||
Message = "A custom 'OnPreToolUse' hook is configured on the SessionConfig, so {Count} approval-required tool(s) ({Tools}) " +
|
||||
"will not be automatically gated by GitHubCopilotAgent. The custom hook is responsible for enforcing approval " +
|
||||
"(for example, by returning a 'deny' or 'ask' PreToolUseHookOutput).")]
|
||||
public static partial void LogApprovalGatingSkippedDueToCustomHook(
|
||||
this ILogger logger,
|
||||
int count,
|
||||
string tools);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.GitHub.Copilot;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a session for a GitHub Copilot agent conversation.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{DebuggerDisplay,nq}")]
|
||||
public sealed class GitHubCopilotAgentSession : AgentSession
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the session ID for the GitHub Copilot conversation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("sessionId")]
|
||||
public string? SessionId { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GitHubCopilotAgentSession"/> class.
|
||||
/// </summary>
|
||||
internal GitHubCopilotAgentSession()
|
||||
{
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
internal GitHubCopilotAgentSession(string? sessionId, AgentSessionStateBag? stateBag) : base(stateBag ?? new())
|
||||
{
|
||||
this.SessionId = sessionId;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
var jso = jsonSerializerOptions ?? GitHubCopilotJsonUtilities.DefaultOptions;
|
||||
return JsonSerializer.SerializeToElement(this, jso.GetTypeInfo(typeof(GitHubCopilotAgentSession)));
|
||||
}
|
||||
|
||||
internal static GitHubCopilotAgentSession Deserialize(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
if (serializedState.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw new ArgumentException("The serialized session state must be a JSON object.", nameof(serializedState));
|
||||
}
|
||||
|
||||
var jso = jsonSerializerOptions ?? GitHubCopilotJsonUtilities.DefaultOptions;
|
||||
return serializedState.Deserialize(jso.GetTypeInfo(typeof(GitHubCopilotAgentSession))) as GitHubCopilotAgentSession
|
||||
?? new GitHubCopilotAgentSession();
|
||||
}
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private string DebuggerDisplay =>
|
||||
$"SessionId = {this.SessionId}, StateBag Count = {this.StateBag.Count}";
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.GitHub.Copilot;
|
||||
|
||||
/// <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))]
|
||||
[JsonSerializable(typeof(Dictionary<string, object?>))]
|
||||
[ExcludeFromCodeCoverage]
|
||||
private sealed partial class JsonContext : JsonSerializerContext;
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsReleaseCandidate>true</IsReleaseCandidate>
|
||||
<!-- GitHub.Copilot.SDK only supports .NET 8.0+ -->
|
||||
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
|
||||
<NoWarn>$(NoWarn);GHCP001</NoWarn>
|
||||
</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>
|
||||
<InternalsVisibleTo Include="Microsoft.Agents.AI.GitHub.Copilot.UnitTests" />
|
||||
</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>
|
||||
|
||||
<!--
|
||||
buildTransitive bridge for GitHub.Copilot.SDK's CLI binary-download targets.
|
||||
|
||||
GitHub.Copilot.SDK ships its CLI download targets under build/, which NuGet
|
||||
only auto-imports for projects with a DIRECT PackageReference to the SDK.
|
||||
Consumers of this package (who reference Microsoft.Agents.AI.GitHub.Copilot
|
||||
instead of GitHub.Copilot.SDK directly) would otherwise get only the managed
|
||||
adapter .dll, no copilot.exe in their output, and a runtime failure at the
|
||||
first RunAsync call.
|
||||
|
||||
The targets file in buildTransitive/ is static and contains all of the
|
||||
Condition / Import logic. The .props file generated here just bakes the
|
||||
SDK version this package was built against into a dedicated property
|
||||
($(_MicrosoftAgentsAICopilotSdkPackagedVersion)) — no Condition or
|
||||
inner $(...) reference in the generated file, so we avoid any MSBuild
|
||||
escape gymnastics during string construction. The static targets file
|
||||
then defaults $(_MicrosoftAgentsAICopilotSdkVersion) to the packaged
|
||||
version unless the consumer overrides it.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<None Include="buildTransitive\Microsoft.Agents.AI.GitHub.Copilot.targets" Pack="true" PackagePath="buildTransitive\" />
|
||||
<None Include="buildTransitive\Microsoft.Agents.AI.GitHub.Copilot.props" Pack="true" PackagePath="buildTransitive\" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="_GenerateBuildTransitiveProps" BeforeTargets="Build;Pack;_GetPackageFiles">
|
||||
<ItemGroup>
|
||||
<_CopilotSdkPackageVersion Include="@(PackageVersion)" Condition="'%(Identity)' == 'GitHub.Copilot.SDK'" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<_CopilotSdkResolvedVersion>@(_CopilotSdkPackageVersion->'%(Version)')</_CopilotSdkResolvedVersion>
|
||||
</PropertyGroup>
|
||||
<Error Condition="'$(_CopilotSdkResolvedVersion)' == ''"
|
||||
Text="Could not resolve GitHub.Copilot.SDK version from PackageVersion items. Ensure the central package version is declared in Directory.Packages.props." />
|
||||
<PropertyGroup>
|
||||
<_BuildTransitivePropsContent>
|
||||
<![CDATA[<Project>
|
||||
<PropertyGroup>
|
||||
<_MicrosoftAgentsAICopilotSdkPackagedVersion>$(_CopilotSdkResolvedVersion)</_MicrosoftAgentsAICopilotSdkPackagedVersion>
|
||||
</PropertyGroup>
|
||||
</Project>]]>
|
||||
</_BuildTransitivePropsContent>
|
||||
</PropertyGroup>
|
||||
<WriteLinesToFile File="$(MSBuildThisFileDirectory)buildTransitive\Microsoft.Agents.AI.GitHub.Copilot.props"
|
||||
Lines="$(_BuildTransitivePropsContent)"
|
||||
Overwrite="true"
|
||||
WriteOnlyWhenDifferent="true" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,3 @@
|
||||
# Auto-generated at pack time by _GenerateBuildTransitiveProps in the csproj.
|
||||
Microsoft.Agents.AI.GitHub.Copilot.props
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<Project>
|
||||
<!--
|
||||
Bridge GitHub.Copilot.SDK's build/ targets to transitive consumers.
|
||||
|
||||
GitHub.Copilot.SDK ships its CLI download / binary-copy MSBuild targets under
|
||||
build/, which means they only auto-import for projects with a DIRECT
|
||||
PackageReference to GitHub.Copilot.SDK. Consumers of this package
|
||||
(Microsoft.Agents.AI.GitHub.Copilot) would otherwise get only the managed
|
||||
adapter .dll, no copilot CLI binary in their output, and the SDK would fail
|
||||
at runtime with:
|
||||
|
||||
Copilot CLI not found at 'bin/{config}/{tfm}/runtimes/{rid}/native/copilot.exe'
|
||||
|
||||
This file ships under buildTransitive/ so NuGet auto-imports it for every
|
||||
transitive consumer, locates the SDK in the NuGet package cache, and imports
|
||||
the SDK's build/ targets so the CLI binary gets downloaded and copied to the
|
||||
consumer's output as expected.
|
||||
|
||||
The companion .props file is generated at this package's pack time and sets
|
||||
$(_MicrosoftAgentsAICopilotSdkPackagedVersion) to the SDK version this
|
||||
package was built against. The Condition below uses that as the default for
|
||||
$(_MicrosoftAgentsAICopilotSdkVersion), so consumers may override the SDK
|
||||
version path by setting $(_MicrosoftAgentsAICopilotSdkVersion) before this
|
||||
file is imported. Consumers may also opt out of the binary download via the
|
||||
SDK's own $(CopilotSkipCliDownload)=true, which the SDK targets honor.
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<_MicrosoftAgentsAICopilotSdkVersion Condition="'$(_MicrosoftAgentsAICopilotSdkVersion)' == ''">$(_MicrosoftAgentsAICopilotSdkPackagedVersion)</_MicrosoftAgentsAICopilotSdkVersion>
|
||||
<_MicrosoftAgentsAICopilotSdkTargetsPath Condition="'$(_MicrosoftAgentsAICopilotSdkVersion)' != ''">$([System.IO.Path]::Combine('$(NuGetPackageRoot)', 'github.copilot.sdk', '$(_MicrosoftAgentsAICopilotSdkVersion)', 'build', 'GitHub.Copilot.SDK.targets'))</_MicrosoftAgentsAICopilotSdkTargetsPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(_MicrosoftAgentsAICopilotSdkTargetsPath)"
|
||||
Condition="'$(_MicrosoftAgentsAICopilotSdkTargetsPath)' != '' And Exists('$(_MicrosoftAgentsAICopilotSdkTargetsPath)')" />
|
||||
</Project>
|
||||
Reference in New Issue
Block a user