mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-27 21:33:58 +02:00
refactor: add provider abstraction to sidecar host
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,288 @@
|
|||||||
|
using GitHub.Copilot.SDK;
|
||||||
|
using GitHub.Copilot.SDK.Rpc;
|
||||||
|
using Aryx.AgentHost.Contracts;
|
||||||
|
|
||||||
|
namespace Aryx.AgentHost.Services;
|
||||||
|
|
||||||
|
internal sealed class CopilotAgentProvider : IAgentProvider
|
||||||
|
{
|
||||||
|
private const string AskUserToolName = "ask_user";
|
||||||
|
private static readonly HashSet<string> ExcludedRuntimeToolNames = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
AskUserToolName,
|
||||||
|
"report_intent",
|
||||||
|
"task_complete",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly string[] AuthenticationErrorIndicators =
|
||||||
|
[
|
||||||
|
"login",
|
||||||
|
"log in",
|
||||||
|
"sign in",
|
||||||
|
"authenticate",
|
||||||
|
"authentication",
|
||||||
|
"not signed in",
|
||||||
|
"not logged in",
|
||||||
|
"reauth",
|
||||||
|
"credential",
|
||||||
|
];
|
||||||
|
|
||||||
|
public ITurnWorkflowRunner CreateWorkflowRunner(WorkflowValidator workflowValidator)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(workflowValidator);
|
||||||
|
return new CopilotWorkflowRunner(workflowValidator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<SidecarCapabilitiesDto> GetCapabilitiesAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return BuildCapabilitiesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IProviderSessionManager CreateSessionManager()
|
||||||
|
{
|
||||||
|
return new CopilotSessionManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<SidecarCapabilitiesDto> BuildCapabilitiesAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CopilotCliContext cliContext = CopilotCliPathResolver.ResolveCliContext();
|
||||||
|
CapabilityProbeResult probe = await ProbeCapabilitiesAsync(cliContext, cancellationToken).ConfigureAwait(false);
|
||||||
|
return CreateCapabilities(probe.Models, probe.RuntimeTools, probe.Connection);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
SidecarConnectionDiagnosticsDto connection = CreateMissingCliDiagnostics(exception);
|
||||||
|
Console.Error.WriteLine($"[aryx sidecar] {connection.Summary} {exception.Message}");
|
||||||
|
return CreateCapabilities([], [], connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<CapabilityProbeResult> ProbeCapabilitiesAsync(
|
||||||
|
CopilotCliContext cliContext,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
IReadOnlyList<SidecarModelCapabilityDto> models = [];
|
||||||
|
IReadOnlyList<SidecarRuntimeToolDto> runtimeTools = [];
|
||||||
|
SidecarCopilotAccountDiagnosticsDto? account = null;
|
||||||
|
SidecarCopilotCliVersionDiagnosticsDto? cliVersion = null;
|
||||||
|
Task<SidecarCopilotCliVersionDiagnosticsDto> cliVersionTask =
|
||||||
|
CopilotConnectionMetadataResolver.GetCliVersionDiagnosticsAsync(cliContext, cancellationToken);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CopilotClientOptions clientOptions = CopilotCliPathResolver.CreateClientOptions(cliContext);
|
||||||
|
|
||||||
|
await using CopilotClient client = new(clientOptions);
|
||||||
|
await client.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
GetAuthStatusResponse? authStatus =
|
||||||
|
await CopilotConnectionMetadataResolver.TryGetAuthStatusAsync(client, cancellationToken).ConfigureAwait(false);
|
||||||
|
account = await CopilotConnectionMetadataResolver.CreateAccountDiagnosticsAsync(
|
||||||
|
authStatus,
|
||||||
|
cliContext.Environment,
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
models = await ListAvailableModelsAsync(client, cancellationToken).ConfigureAwait(false);
|
||||||
|
runtimeTools = await TryListAvailableRuntimeToolsAsync(client, cancellationToken).ConfigureAwait(false);
|
||||||
|
cliVersion = await cliVersionTask.ConfigureAwait(false);
|
||||||
|
|
||||||
|
return new CapabilityProbeResult(
|
||||||
|
models,
|
||||||
|
runtimeTools,
|
||||||
|
CreateReadyConnectionDiagnostics(cliContext.CliPath, models.Count, cliVersion, account));
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
cliVersion = await cliVersionTask.ConfigureAwait(false);
|
||||||
|
Console.Error.WriteLine($"[aryx sidecar] Failed to list available Copilot models: {exception.Message}");
|
||||||
|
|
||||||
|
return new CapabilityProbeResult(
|
||||||
|
models,
|
||||||
|
runtimeTools,
|
||||||
|
CreateFailureConnectionDiagnostics(cliContext.CliPath, exception, cliVersion, account));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SidecarCapabilitiesDto CreateCapabilities(
|
||||||
|
IReadOnlyList<SidecarModelCapabilityDto> models,
|
||||||
|
IReadOnlyList<SidecarRuntimeToolDto> runtimeTools,
|
||||||
|
SidecarConnectionDiagnosticsDto connection)
|
||||||
|
{
|
||||||
|
return new SidecarCapabilitiesDto
|
||||||
|
{
|
||||||
|
Modes = BuildModeCapabilities(),
|
||||||
|
Models = models,
|
||||||
|
RuntimeTools = runtimeTools,
|
||||||
|
Connection = connection,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, SidecarModeCapabilityDto> BuildModeCapabilities()
|
||||||
|
{
|
||||||
|
return new Dictionary<string, SidecarModeCapabilityDto>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["single"] = new() { Available = true },
|
||||||
|
["sequential"] = new() { Available = true },
|
||||||
|
["concurrent"] = new() { Available = true },
|
||||||
|
["handoff"] = new() { Available = true },
|
||||||
|
["group-chat"] = new() { Available = true },
|
||||||
|
["magentic"] = new()
|
||||||
|
{
|
||||||
|
Available = false,
|
||||||
|
Reason = "Microsoft Agent Framework currently documents Magentic orchestration as unsupported in C#.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IReadOnlyList<SidecarModelCapabilityDto>> ListAvailableModelsAsync(
|
||||||
|
CopilotClient client,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
List<ModelInfo> models = await client.ListModelsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
return models
|
||||||
|
.Select(model => new SidecarModelCapabilityDto
|
||||||
|
{
|
||||||
|
Id = model.Id,
|
||||||
|
Name = model.Name,
|
||||||
|
SupportedReasoningEfforts = (model.SupportedReasoningEfforts ?? [])
|
||||||
|
.Where(IsReasoningEffort)
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.ToList(),
|
||||||
|
DefaultReasoningEffort = IsReasoningEffort(model.DefaultReasoningEffort)
|
||||||
|
? model.DefaultReasoningEffort
|
||||||
|
: null,
|
||||||
|
})
|
||||||
|
.OrderBy(model => model.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IReadOnlyList<SidecarRuntimeToolDto>> TryListAvailableRuntimeToolsAsync(
|
||||||
|
CopilotClient client,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await ListAvailableRuntimeToolsAsync(client, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[aryx sidecar] Failed to list available Copilot runtime tools: {exception.Message}");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IReadOnlyList<SidecarRuntimeToolDto>> ListAvailableRuntimeToolsAsync(
|
||||||
|
CopilotClient client,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ToolsListResult result = await client.Rpc.Tools.ListAsync(null!, cancellationToken).ConfigureAwait(false);
|
||||||
|
return MapRuntimeTools(result.Tools);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static IReadOnlyList<SidecarRuntimeToolDto> MapRuntimeTools(IEnumerable<Tool> tools)
|
||||||
|
{
|
||||||
|
return tools
|
||||||
|
.Where(ShouldIncludeRuntimeTool)
|
||||||
|
.Where(tool => !string.IsNullOrWhiteSpace(tool.Name))
|
||||||
|
.Select(tool => new SidecarRuntimeToolDto
|
||||||
|
{
|
||||||
|
Id = tool.Name.Trim(),
|
||||||
|
Label = tool.Name.Trim(),
|
||||||
|
Description = string.IsNullOrWhiteSpace(tool.Description) ? null : tool.Description.Trim(),
|
||||||
|
})
|
||||||
|
.DistinctBy(tool => tool.Id, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.OrderBy(tool => tool.Label, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldIncludeRuntimeTool(Tool tool)
|
||||||
|
{
|
||||||
|
string? toolName = string.IsNullOrWhiteSpace(tool.Name) ? null : tool.Name.Trim();
|
||||||
|
return toolName is not null
|
||||||
|
&& !ExcludedRuntimeToolNames.Contains(toolName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsReasoningEffort(string? value)
|
||||||
|
{
|
||||||
|
return value is "low" or "medium" or "high" or "xhigh";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static SidecarConnectionDiagnosticsDto CreateMissingCliDiagnostics(Exception exception)
|
||||||
|
{
|
||||||
|
return new SidecarConnectionDiagnosticsDto
|
||||||
|
{
|
||||||
|
Status = "copilot-cli-missing",
|
||||||
|
Summary = "GitHub Copilot CLI is not installed or is not available on PATH.",
|
||||||
|
Detail = exception.Message,
|
||||||
|
CheckedAt = DateTimeOffset.UtcNow.ToString("O"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static SidecarConnectionDiagnosticsDto CreateReadyConnectionDiagnostics(
|
||||||
|
string cliPath,
|
||||||
|
int modelCount,
|
||||||
|
SidecarCopilotCliVersionDiagnosticsDto? cliVersion = null,
|
||||||
|
SidecarCopilotAccountDiagnosticsDto? account = null)
|
||||||
|
{
|
||||||
|
string summary = modelCount switch
|
||||||
|
{
|
||||||
|
0 => "Connected to GitHub Copilot, but no models were reported.",
|
||||||
|
1 => "Connected to GitHub Copilot. 1 model is available.",
|
||||||
|
_ => $"Connected to GitHub Copilot. {modelCount} models are available.",
|
||||||
|
};
|
||||||
|
|
||||||
|
return new SidecarConnectionDiagnosticsDto
|
||||||
|
{
|
||||||
|
Status = "ready",
|
||||||
|
Summary = summary,
|
||||||
|
Detail = $"Using Copilot CLI at {cliPath}.",
|
||||||
|
CopilotCliPath = cliPath,
|
||||||
|
CopilotCliVersion = cliVersion,
|
||||||
|
Account = account,
|
||||||
|
CheckedAt = DateTimeOffset.UtcNow.ToString("O"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static SidecarConnectionDiagnosticsDto CreateFailureConnectionDiagnostics(
|
||||||
|
string? cliPath,
|
||||||
|
Exception exception,
|
||||||
|
SidecarCopilotCliVersionDiagnosticsDto? cliVersion = null,
|
||||||
|
SidecarCopilotAccountDiagnosticsDto? account = null)
|
||||||
|
{
|
||||||
|
string status = ClassifyConnectionStatus(exception);
|
||||||
|
string summary = status == "copilot-auth-required"
|
||||||
|
? "GitHub Copilot requires authentication before Aryx can load models."
|
||||||
|
: "GitHub Copilot was found, but Aryx could not load its model list.";
|
||||||
|
|
||||||
|
return new SidecarConnectionDiagnosticsDto
|
||||||
|
{
|
||||||
|
Status = status,
|
||||||
|
Summary = summary,
|
||||||
|
Detail = exception.Message,
|
||||||
|
CopilotCliPath = cliPath,
|
||||||
|
CopilotCliVersion = cliVersion,
|
||||||
|
Account = account,
|
||||||
|
CheckedAt = DateTimeOffset.UtcNow.ToString("O"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string ClassifyConnectionStatus(Exception exception)
|
||||||
|
{
|
||||||
|
string message = exception.Message;
|
||||||
|
if (AuthenticationErrorIndicators.Any(indicator =>
|
||||||
|
message.Contains(indicator, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
return "copilot-auth-required";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "copilot-error";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record CapabilityProbeResult(
|
||||||
|
IReadOnlyList<SidecarModelCapabilityDto> Models,
|
||||||
|
IReadOnlyList<SidecarRuntimeToolDto> RuntimeTools,
|
||||||
|
SidecarConnectionDiagnosticsDto Connection);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Aryx.AgentHost.Contracts;
|
||||||
|
|
||||||
|
namespace Aryx.AgentHost.Services;
|
||||||
|
|
||||||
|
internal interface IAgentProvider
|
||||||
|
{
|
||||||
|
ITurnWorkflowRunner CreateWorkflowRunner(WorkflowValidator workflowValidator);
|
||||||
|
|
||||||
|
Task<SidecarCapabilitiesDto> GetCapabilitiesAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
IProviderSessionManager CreateSessionManager();
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ using Aryx.AgentHost.Contracts;
|
|||||||
|
|
||||||
namespace Aryx.AgentHost.Services;
|
namespace Aryx.AgentHost.Services;
|
||||||
|
|
||||||
public interface ICopilotSessionManager
|
public interface IProviderSessionManager
|
||||||
{
|
{
|
||||||
Task<IReadOnlyList<CopilotSessionInfoDto>> ListSessionsAsync(
|
Task<IReadOnlyList<CopilotSessionInfoDto>> ListSessionsAsync(
|
||||||
CopilotSessionListFilterDto? filter,
|
CopilotSessionListFilterDto? filter,
|
||||||
@@ -17,3 +17,5 @@ public interface ICopilotSessionManager
|
|||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface ICopilotSessionManager : IProviderSessionManager;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using GitHub.Copilot.SDK;
|
|
||||||
using GitHub.Copilot.SDK.Rpc;
|
|
||||||
using Aryx.AgentHost.Contracts;
|
using Aryx.AgentHost.Contracts;
|
||||||
|
|
||||||
namespace Aryx.AgentHost.Services;
|
namespace Aryx.AgentHost.Services;
|
||||||
@@ -19,31 +17,11 @@ public sealed class SidecarProtocolHost
|
|||||||
private const string DeleteSessionCommandType = "delete-session";
|
private const string DeleteSessionCommandType = "delete-session";
|
||||||
private const string DisconnectSessionCommandType = "disconnect-session";
|
private const string DisconnectSessionCommandType = "disconnect-session";
|
||||||
private const string GetQuotaCommandType = "get-quota";
|
private const string GetQuotaCommandType = "get-quota";
|
||||||
private const string AskUserToolName = "ask_user";
|
|
||||||
private static readonly HashSet<string> ExcludedRuntimeToolNames = new(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
AskUserToolName,
|
|
||||||
"report_intent",
|
|
||||||
"task_complete",
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly string[] AuthenticationErrorIndicators =
|
|
||||||
[
|
|
||||||
"login",
|
|
||||||
"log in",
|
|
||||||
"sign in",
|
|
||||||
"authenticate",
|
|
||||||
"authentication",
|
|
||||||
"not signed in",
|
|
||||||
"not logged in",
|
|
||||||
"reauth",
|
|
||||||
"credential",
|
|
||||||
];
|
|
||||||
|
|
||||||
private readonly Func<CancellationToken, Task<SidecarCapabilitiesDto>> _capabilitiesProvider;
|
private readonly Func<CancellationToken, Task<SidecarCapabilitiesDto>> _capabilitiesProvider;
|
||||||
private readonly WorkflowValidator _workflowValidator;
|
private readonly WorkflowValidator _workflowValidator;
|
||||||
private readonly ITurnWorkflowRunner _workflowRunner;
|
private readonly ITurnWorkflowRunner _workflowRunner;
|
||||||
private readonly ICopilotSessionManager _sessionManager;
|
private readonly IProviderSessionManager _sessionManager;
|
||||||
private readonly JsonSerializerOptions _jsonOptions;
|
private readonly JsonSerializerOptions _jsonOptions;
|
||||||
private readonly IReadOnlyDictionary<string, Func<CommandContext, Task>> _commandHandlers;
|
private readonly IReadOnlyDictionary<string, Func<CommandContext, Task>> _commandHandlers;
|
||||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||||
@@ -60,7 +38,7 @@ public sealed class SidecarProtocolHost
|
|||||||
public SidecarProtocolHost(
|
public SidecarProtocolHost(
|
||||||
ITurnWorkflowRunner? workflowRunner = null,
|
ITurnWorkflowRunner? workflowRunner = null,
|
||||||
Func<CancellationToken, Task<SidecarCapabilitiesDto>>? capabilitiesProvider = null,
|
Func<CancellationToken, Task<SidecarCapabilitiesDto>>? capabilitiesProvider = null,
|
||||||
ICopilotSessionManager? sessionManager = null)
|
IProviderSessionManager? sessionManager = null)
|
||||||
: this(new WorkflowValidator(), workflowRunner, capabilitiesProvider, sessionManager)
|
: this(new WorkflowValidator(), workflowRunner, capabilitiesProvider, sessionManager)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -69,12 +47,25 @@ public sealed class SidecarProtocolHost
|
|||||||
WorkflowValidator workflowValidator,
|
WorkflowValidator workflowValidator,
|
||||||
ITurnWorkflowRunner? workflowRunner = null,
|
ITurnWorkflowRunner? workflowRunner = null,
|
||||||
Func<CancellationToken, Task<SidecarCapabilitiesDto>>? capabilitiesProvider = null,
|
Func<CancellationToken, Task<SidecarCapabilitiesDto>>? capabilitiesProvider = null,
|
||||||
ICopilotSessionManager? sessionManager = null)
|
IProviderSessionManager? sessionManager = null)
|
||||||
|
: this(workflowValidator, new CopilotAgentProvider(), workflowRunner, capabilitiesProvider, sessionManager)
|
||||||
{
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal SidecarProtocolHost(
|
||||||
|
WorkflowValidator workflowValidator,
|
||||||
|
IAgentProvider agentProvider,
|
||||||
|
ITurnWorkflowRunner? workflowRunner = null,
|
||||||
|
Func<CancellationToken, Task<SidecarCapabilitiesDto>>? capabilitiesProvider = null,
|
||||||
|
IProviderSessionManager? sessionManager = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(workflowValidator);
|
||||||
|
ArgumentNullException.ThrowIfNull(agentProvider);
|
||||||
|
|
||||||
_workflowValidator = workflowValidator;
|
_workflowValidator = workflowValidator;
|
||||||
_workflowRunner = workflowRunner ?? new CopilotWorkflowRunner(_workflowValidator);
|
_workflowRunner = workflowRunner ?? agentProvider.CreateWorkflowRunner(_workflowValidator);
|
||||||
_capabilitiesProvider = capabilitiesProvider ?? BuildCapabilitiesAsync;
|
_capabilitiesProvider = capabilitiesProvider ?? agentProvider.GetCapabilitiesAsync;
|
||||||
_sessionManager = sessionManager ?? new CopilotSessionManager();
|
_sessionManager = sessionManager ?? agentProvider.CreateSessionManager();
|
||||||
_jsonOptions = JsonSerialization.CreateWebOptions();
|
_jsonOptions = JsonSerialization.CreateWebOptions();
|
||||||
_jsonOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
_jsonOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||||
_jsonOptions.PropertyNameCaseInsensitive = true;
|
_jsonOptions.PropertyNameCaseInsensitive = true;
|
||||||
@@ -459,252 +450,9 @@ public sealed class SidecarProtocolHost
|
|||||||
return cancelledRequestIds;
|
return cancelledRequestIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<SidecarCapabilitiesDto> BuildCapabilitiesAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
CopilotCliContext cliContext = CopilotCliPathResolver.ResolveCliContext();
|
|
||||||
CapabilityProbeResult probe = await ProbeCapabilitiesAsync(cliContext, cancellationToken).ConfigureAwait(false);
|
|
||||||
return CreateCapabilities(probe.Models, probe.RuntimeTools, probe.Connection);
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
SidecarConnectionDiagnosticsDto connection = CreateMissingCliDiagnostics(exception);
|
|
||||||
Console.Error.WriteLine($"[aryx sidecar] {connection.Summary} {exception.Message}");
|
|
||||||
return CreateCapabilities([], [], connection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<CapabilityProbeResult> ProbeCapabilitiesAsync(
|
|
||||||
CopilotCliContext cliContext,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
IReadOnlyList<SidecarModelCapabilityDto> models = [];
|
|
||||||
IReadOnlyList<SidecarRuntimeToolDto> runtimeTools = [];
|
|
||||||
SidecarCopilotAccountDiagnosticsDto? account = null;
|
|
||||||
SidecarCopilotCliVersionDiagnosticsDto? cliVersion = null;
|
|
||||||
Task<SidecarCopilotCliVersionDiagnosticsDto> cliVersionTask =
|
|
||||||
CopilotConnectionMetadataResolver.GetCliVersionDiagnosticsAsync(cliContext, cancellationToken);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
CopilotClientOptions clientOptions = CopilotCliPathResolver.CreateClientOptions(cliContext);
|
|
||||||
|
|
||||||
await using CopilotClient client = new(clientOptions);
|
|
||||||
await client.StartAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
GetAuthStatusResponse? authStatus =
|
|
||||||
await CopilotConnectionMetadataResolver.TryGetAuthStatusAsync(client, cancellationToken).ConfigureAwait(false);
|
|
||||||
account = await CopilotConnectionMetadataResolver.CreateAccountDiagnosticsAsync(
|
|
||||||
authStatus,
|
|
||||||
cliContext.Environment,
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
models = await ListAvailableModelsAsync(client, cancellationToken).ConfigureAwait(false);
|
|
||||||
runtimeTools = await TryListAvailableRuntimeToolsAsync(client, cancellationToken).ConfigureAwait(false);
|
|
||||||
cliVersion = await cliVersionTask.ConfigureAwait(false);
|
|
||||||
|
|
||||||
return new CapabilityProbeResult(
|
|
||||||
models,
|
|
||||||
runtimeTools,
|
|
||||||
CreateReadyConnectionDiagnostics(cliContext.CliPath, models.Count, cliVersion, account));
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
cliVersion = await cliVersionTask.ConfigureAwait(false);
|
|
||||||
Console.Error.WriteLine($"[aryx sidecar] Failed to list available Copilot models: {exception.Message}");
|
|
||||||
|
|
||||||
return new CapabilityProbeResult(
|
|
||||||
models,
|
|
||||||
runtimeTools,
|
|
||||||
CreateFailureConnectionDiagnostics(cliContext.CliPath, exception, cliVersion, account));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SidecarCapabilitiesDto CreateCapabilities(
|
|
||||||
IReadOnlyList<SidecarModelCapabilityDto> models,
|
|
||||||
IReadOnlyList<SidecarRuntimeToolDto> runtimeTools,
|
|
||||||
SidecarConnectionDiagnosticsDto connection)
|
|
||||||
{
|
|
||||||
return new SidecarCapabilitiesDto
|
|
||||||
{
|
|
||||||
Modes = BuildModeCapabilities(),
|
|
||||||
Models = models,
|
|
||||||
RuntimeTools = runtimeTools,
|
|
||||||
Connection = connection,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Dictionary<string, SidecarModeCapabilityDto> BuildModeCapabilities()
|
|
||||||
{
|
|
||||||
return new Dictionary<string, SidecarModeCapabilityDto>(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
["single"] = new() { Available = true },
|
|
||||||
["sequential"] = new() { Available = true },
|
|
||||||
["concurrent"] = new() { Available = true },
|
|
||||||
["handoff"] = new() { Available = true },
|
|
||||||
["group-chat"] = new() { Available = true },
|
|
||||||
["magentic"] = new()
|
|
||||||
{
|
|
||||||
Available = false,
|
|
||||||
Reason = "Microsoft Agent Framework currently documents Magentic orchestration as unsupported in C#.",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IReadOnlyList<SidecarModelCapabilityDto>> ListAvailableModelsAsync(
|
|
||||||
CopilotClient client,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
List<ModelInfo> models = await client.ListModelsAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
return models
|
|
||||||
.Select(model => new SidecarModelCapabilityDto
|
|
||||||
{
|
|
||||||
Id = model.Id,
|
|
||||||
Name = model.Name,
|
|
||||||
SupportedReasoningEfforts = (model.SupportedReasoningEfforts ?? [])
|
|
||||||
.Where(IsReasoningEffort)
|
|
||||||
.Distinct(StringComparer.Ordinal)
|
|
||||||
.ToList(),
|
|
||||||
DefaultReasoningEffort = IsReasoningEffort(model.DefaultReasoningEffort)
|
|
||||||
? model.DefaultReasoningEffort
|
|
||||||
: null,
|
|
||||||
})
|
|
||||||
.OrderBy(model => model.Name, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IReadOnlyList<SidecarRuntimeToolDto>> TryListAvailableRuntimeToolsAsync(
|
|
||||||
CopilotClient client,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await ListAvailableRuntimeToolsAsync(client, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"[aryx sidecar] Failed to list available Copilot runtime tools: {exception.Message}");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IReadOnlyList<SidecarRuntimeToolDto>> ListAvailableRuntimeToolsAsync(
|
|
||||||
CopilotClient client,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
ToolsListResult result = await client.Rpc.Tools.ListAsync(null!, cancellationToken).ConfigureAwait(false);
|
|
||||||
return MapRuntimeTools(result.Tools);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static IReadOnlyList<SidecarRuntimeToolDto> MapRuntimeTools(IEnumerable<Tool> tools)
|
|
||||||
{
|
|
||||||
return tools
|
|
||||||
.Where(ShouldIncludeRuntimeTool)
|
|
||||||
.Where(tool => !string.IsNullOrWhiteSpace(tool.Name))
|
|
||||||
.Select(tool => new SidecarRuntimeToolDto
|
|
||||||
{
|
|
||||||
Id = tool.Name.Trim(),
|
|
||||||
Label = tool.Name.Trim(),
|
|
||||||
Description = string.IsNullOrWhiteSpace(tool.Description) ? null : tool.Description.Trim(),
|
|
||||||
})
|
|
||||||
.DistinctBy(tool => tool.Id, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.OrderBy(tool => tool.Label, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool ShouldIncludeRuntimeTool(Tool tool)
|
|
||||||
{
|
|
||||||
string? toolName = string.IsNullOrWhiteSpace(tool.Name) ? null : tool.Name.Trim();
|
|
||||||
return toolName is not null
|
|
||||||
&& !ExcludedRuntimeToolNames.Contains(toolName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsReasoningEffort(string? value)
|
|
||||||
{
|
|
||||||
return value is "low" or "medium" or "high" or "xhigh";
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static SidecarConnectionDiagnosticsDto CreateMissingCliDiagnostics(Exception exception)
|
|
||||||
{
|
|
||||||
return new SidecarConnectionDiagnosticsDto
|
|
||||||
{
|
|
||||||
Status = "copilot-cli-missing",
|
|
||||||
Summary = "GitHub Copilot CLI is not installed or is not available on PATH.",
|
|
||||||
Detail = exception.Message,
|
|
||||||
CheckedAt = DateTimeOffset.UtcNow.ToString("O"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static SidecarConnectionDiagnosticsDto CreateReadyConnectionDiagnostics(
|
|
||||||
string cliPath,
|
|
||||||
int modelCount,
|
|
||||||
SidecarCopilotCliVersionDiagnosticsDto? cliVersion = null,
|
|
||||||
SidecarCopilotAccountDiagnosticsDto? account = null)
|
|
||||||
{
|
|
||||||
string summary = modelCount switch
|
|
||||||
{
|
|
||||||
0 => "Connected to GitHub Copilot, but no models were reported.",
|
|
||||||
1 => "Connected to GitHub Copilot. 1 model is available.",
|
|
||||||
_ => $"Connected to GitHub Copilot. {modelCount} models are available.",
|
|
||||||
};
|
|
||||||
|
|
||||||
return new SidecarConnectionDiagnosticsDto
|
|
||||||
{
|
|
||||||
Status = "ready",
|
|
||||||
Summary = summary,
|
|
||||||
Detail = $"Using Copilot CLI at {cliPath}.",
|
|
||||||
CopilotCliPath = cliPath,
|
|
||||||
CopilotCliVersion = cliVersion,
|
|
||||||
Account = account,
|
|
||||||
CheckedAt = DateTimeOffset.UtcNow.ToString("O"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static SidecarConnectionDiagnosticsDto CreateFailureConnectionDiagnostics(
|
|
||||||
string? cliPath,
|
|
||||||
Exception exception,
|
|
||||||
SidecarCopilotCliVersionDiagnosticsDto? cliVersion = null,
|
|
||||||
SidecarCopilotAccountDiagnosticsDto? account = null)
|
|
||||||
{
|
|
||||||
string status = ClassifyConnectionStatus(exception);
|
|
||||||
string summary = status == "copilot-auth-required"
|
|
||||||
? "GitHub Copilot requires authentication before Aryx can load models."
|
|
||||||
: "GitHub Copilot was found, but Aryx could not load its model list.";
|
|
||||||
|
|
||||||
return new SidecarConnectionDiagnosticsDto
|
|
||||||
{
|
|
||||||
Status = status,
|
|
||||||
Summary = summary,
|
|
||||||
Detail = exception.Message,
|
|
||||||
CopilotCliPath = cliPath,
|
|
||||||
CopilotCliVersion = cliVersion,
|
|
||||||
Account = account,
|
|
||||||
CheckedAt = DateTimeOffset.UtcNow.ToString("O"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static string ClassifyConnectionStatus(Exception exception)
|
|
||||||
{
|
|
||||||
string message = exception.Message;
|
|
||||||
if (AuthenticationErrorIndicators.Any(indicator =>
|
|
||||||
message.Contains(indicator, StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
|
||||||
return "copilot-auth-required";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "copilot-error";
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed record CommandContext(
|
private sealed record CommandContext(
|
||||||
string RawCommand,
|
string RawCommand,
|
||||||
SidecarCommandEnvelope Envelope,
|
SidecarCommandEnvelope Envelope,
|
||||||
TextWriter Output,
|
TextWriter Output,
|
||||||
CancellationToken CancellationToken);
|
CancellationToken CancellationToken);
|
||||||
|
|
||||||
private sealed record CapabilityProbeResult(
|
|
||||||
IReadOnlyList<SidecarModelCapabilityDto> Models,
|
|
||||||
IReadOnlyList<SidecarRuntimeToolDto> RuntimeTools,
|
|
||||||
SidecarConnectionDiagnosticsDto Connection);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,103 @@ public sealed class SidecarProtocolHostTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InternalConstructor_UsesAgentProviderDefaults()
|
||||||
|
{
|
||||||
|
FakeWorkflowRunner workflowRunner = new(async (command, onDelta, onActivity, onApproval, onUserInput, onMcpOAuthRequired, onExitPlanMode, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await onActivity(new AgentActivityEventDto
|
||||||
|
{
|
||||||
|
Type = "agent-activity",
|
||||||
|
RequestId = command.RequestId,
|
||||||
|
SessionId = command.SessionId,
|
||||||
|
ActivityType = "thinking",
|
||||||
|
AgentId = "agent-provider",
|
||||||
|
AgentName = "Provider Agent",
|
||||||
|
});
|
||||||
|
|
||||||
|
return
|
||||||
|
[
|
||||||
|
new ChatMessageDto
|
||||||
|
{
|
||||||
|
Id = "assistant-provider",
|
||||||
|
Role = "assistant",
|
||||||
|
AuthorName = "Provider Agent",
|
||||||
|
Content = "Hello from the provider.",
|
||||||
|
CreatedAt = "2026-01-01T00:00:00.0000000Z",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
FakeSessionManager sessionManager = new()
|
||||||
|
{
|
||||||
|
Sessions =
|
||||||
|
[
|
||||||
|
new CopilotSessionInfoDto
|
||||||
|
{
|
||||||
|
CopilotSessionId = "aryx::provider-session::agent-provider",
|
||||||
|
ManagedByAryx = true,
|
||||||
|
SessionId = "provider-session",
|
||||||
|
AgentId = "agent-provider",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
SidecarCapabilitiesDto capabilities = new()
|
||||||
|
{
|
||||||
|
Modes = new Dictionary<string, SidecarModeCapabilityDto>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["single"] = new() { Available = true },
|
||||||
|
},
|
||||||
|
Models =
|
||||||
|
[
|
||||||
|
new SidecarModelCapabilityDto
|
||||||
|
{
|
||||||
|
Id = "provider-model",
|
||||||
|
Name = "Provider Model",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
RuntimeTools = [],
|
||||||
|
Connection = new SidecarConnectionDiagnosticsDto
|
||||||
|
{
|
||||||
|
Status = "ready",
|
||||||
|
Summary = "Provider is ready.",
|
||||||
|
CheckedAt = "2026-01-01T00:00:00.0000000Z",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
SidecarProtocolHost host = new(
|
||||||
|
new WorkflowValidator(),
|
||||||
|
new FakeAgentProvider(workflowRunner, sessionManager, capabilities));
|
||||||
|
|
||||||
|
IReadOnlyList<JsonElement> capabilityEvents = await RunHostAsync(
|
||||||
|
new DescribeCapabilitiesCommandDto
|
||||||
|
{
|
||||||
|
Type = "describe-capabilities",
|
||||||
|
RequestId = "provider-capabilities",
|
||||||
|
},
|
||||||
|
host);
|
||||||
|
IReadOnlyList<JsonElement> sessionEvents = await RunHostAsync(
|
||||||
|
new ListSessionsCommandDto
|
||||||
|
{
|
||||||
|
Type = "list-sessions",
|
||||||
|
RequestId = "provider-sessions",
|
||||||
|
},
|
||||||
|
host);
|
||||||
|
IReadOnlyList<JsonElement> turnEvents = await RunHostAsync(
|
||||||
|
CreateRunTurnCommand(requestId: "provider-turn"),
|
||||||
|
host);
|
||||||
|
|
||||||
|
JsonElement capabilityEvent = AssertSingleEvent(capabilityEvents, "capabilities", "provider-capabilities");
|
||||||
|
JsonElement model = Assert.Single(capabilityEvent.GetProperty("capabilities").GetProperty("models").EnumerateArray());
|
||||||
|
Assert.Equal("provider-model", model.GetProperty("id").GetString());
|
||||||
|
|
||||||
|
JsonElement listedEvent = AssertSingleEvent(sessionEvents, "sessions-listed", "provider-sessions");
|
||||||
|
JsonElement session = Assert.Single(listedEvent.GetProperty("sessions").EnumerateArray());
|
||||||
|
Assert.Equal("provider-session", session.GetProperty("sessionId").GetString());
|
||||||
|
|
||||||
|
JsonElement turnComplete = AssertSingleEvent(turnEvents, "turn-complete", "provider-turn");
|
||||||
|
JsonElement message = Assert.Single(turnComplete.GetProperty("messages").EnumerateArray());
|
||||||
|
Assert.Equal("Hello from the provider.", message.GetProperty("content").GetString());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateWorkflowCommand_ReturnsIssuesAndCompletion()
|
public async Task ValidateWorkflowCommand_ReturnsIssuesAndCompletion()
|
||||||
{
|
{
|
||||||
@@ -668,7 +765,7 @@ public sealed class SidecarProtocolHostTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void MapRuntimeTools_ExcludesOnlyInternalMetaToolsAndDeduplicatesByName()
|
public void MapRuntimeTools_ExcludesOnlyInternalMetaToolsAndDeduplicatesByName()
|
||||||
{
|
{
|
||||||
IReadOnlyList<SidecarRuntimeToolDto> runtimeTools = SidecarProtocolHost.MapRuntimeTools(
|
IReadOnlyList<SidecarRuntimeToolDto> runtimeTools = CopilotAgentProvider.MapRuntimeTools(
|
||||||
[
|
[
|
||||||
new Tool
|
new Tool
|
||||||
{
|
{
|
||||||
@@ -721,7 +818,7 @@ public sealed class SidecarProtocolHostTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ClassifyConnectionStatus_ReturnsAuthRequiredForLoginFailures()
|
public void ClassifyConnectionStatus_ReturnsAuthRequiredForLoginFailures()
|
||||||
{
|
{
|
||||||
string status = SidecarProtocolHost.ClassifyConnectionStatus(
|
string status = CopilotAgentProvider.ClassifyConnectionStatus(
|
||||||
new InvalidOperationException("Please run copilot auth login to continue."));
|
new InvalidOperationException("Please run copilot auth login to continue."));
|
||||||
|
|
||||||
Assert.Equal("copilot-auth-required", status);
|
Assert.Equal("copilot-auth-required", status);
|
||||||
@@ -731,7 +828,7 @@ public sealed class SidecarProtocolHostTests
|
|||||||
public void CreateReadyConnectionDiagnostics_ReportsCliPathAndModelCount()
|
public void CreateReadyConnectionDiagnostics_ReportsCliPathAndModelCount()
|
||||||
{
|
{
|
||||||
SidecarConnectionDiagnosticsDto diagnostics =
|
SidecarConnectionDiagnosticsDto diagnostics =
|
||||||
SidecarProtocolHost.CreateReadyConnectionDiagnostics(
|
CopilotAgentProvider.CreateReadyConnectionDiagnostics(
|
||||||
@"C:\tools\copilot\copilot.exe",
|
@"C:\tools\copilot\copilot.exe",
|
||||||
2,
|
2,
|
||||||
new SidecarCopilotCliVersionDiagnosticsDto
|
new SidecarCopilotCliVersionDiagnosticsDto
|
||||||
@@ -1145,6 +1242,38 @@ public sealed class SidecarProtocolHostTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class FakeAgentProvider : IAgentProvider
|
||||||
|
{
|
||||||
|
private readonly ITurnWorkflowRunner _workflowRunner;
|
||||||
|
private readonly IProviderSessionManager _sessionManager;
|
||||||
|
private readonly SidecarCapabilitiesDto _capabilities;
|
||||||
|
|
||||||
|
public FakeAgentProvider(
|
||||||
|
ITurnWorkflowRunner workflowRunner,
|
||||||
|
IProviderSessionManager sessionManager,
|
||||||
|
SidecarCapabilitiesDto capabilities)
|
||||||
|
{
|
||||||
|
_workflowRunner = workflowRunner;
|
||||||
|
_sessionManager = sessionManager;
|
||||||
|
_capabilities = capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ITurnWorkflowRunner CreateWorkflowRunner(WorkflowValidator workflowValidator)
|
||||||
|
{
|
||||||
|
return _workflowRunner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<SidecarCapabilitiesDto> GetCapabilitiesAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(_capabilities);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IProviderSessionManager CreateSessionManager()
|
||||||
|
{
|
||||||
|
return _sessionManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class FakeSessionManager : ICopilotSessionManager
|
private sealed class FakeSessionManager : ICopilotSessionManager
|
||||||
{
|
{
|
||||||
public IReadOnlyList<CopilotSessionInfoDto> Sessions { get; init; } = [];
|
public IReadOnlyList<CopilotSessionInfoDto> Sessions { get; init; } = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user