mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-03 10:28:43 +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;
|
||||
|
||||
public interface ICopilotSessionManager
|
||||
public interface IProviderSessionManager
|
||||
{
|
||||
Task<IReadOnlyList<CopilotSessionInfoDto>> ListSessionsAsync(
|
||||
CopilotSessionListFilterDto? filter,
|
||||
@@ -17,3 +17,5 @@ public interface ICopilotSessionManager
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public interface ICopilotSessionManager : IProviderSessionManager;
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using GitHub.Copilot.SDK;
|
||||
using GitHub.Copilot.SDK.Rpc;
|
||||
using Aryx.AgentHost.Contracts;
|
||||
|
||||
namespace Aryx.AgentHost.Services;
|
||||
@@ -19,31 +17,11 @@ public sealed class SidecarProtocolHost
|
||||
private const string DeleteSessionCommandType = "delete-session";
|
||||
private const string DisconnectSessionCommandType = "disconnect-session";
|
||||
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 WorkflowValidator _workflowValidator;
|
||||
private readonly ITurnWorkflowRunner _workflowRunner;
|
||||
private readonly ICopilotSessionManager _sessionManager;
|
||||
private readonly IProviderSessionManager _sessionManager;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private readonly IReadOnlyDictionary<string, Func<CommandContext, Task>> _commandHandlers;
|
||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||
@@ -60,7 +38,7 @@ public sealed class SidecarProtocolHost
|
||||
public SidecarProtocolHost(
|
||||
ITurnWorkflowRunner? workflowRunner = null,
|
||||
Func<CancellationToken, Task<SidecarCapabilitiesDto>>? capabilitiesProvider = null,
|
||||
ICopilotSessionManager? sessionManager = null)
|
||||
IProviderSessionManager? sessionManager = null)
|
||||
: this(new WorkflowValidator(), workflowRunner, capabilitiesProvider, sessionManager)
|
||||
{
|
||||
}
|
||||
@@ -69,12 +47,25 @@ public sealed class SidecarProtocolHost
|
||||
WorkflowValidator workflowValidator,
|
||||
ITurnWorkflowRunner? workflowRunner = 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;
|
||||
_workflowRunner = workflowRunner ?? new CopilotWorkflowRunner(_workflowValidator);
|
||||
_capabilitiesProvider = capabilitiesProvider ?? BuildCapabilitiesAsync;
|
||||
_sessionManager = sessionManager ?? new CopilotSessionManager();
|
||||
_workflowRunner = workflowRunner ?? agentProvider.CreateWorkflowRunner(_workflowValidator);
|
||||
_capabilitiesProvider = capabilitiesProvider ?? agentProvider.GetCapabilitiesAsync;
|
||||
_sessionManager = sessionManager ?? agentProvider.CreateSessionManager();
|
||||
_jsonOptions = JsonSerialization.CreateWebOptions();
|
||||
_jsonOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||
_jsonOptions.PropertyNameCaseInsensitive = true;
|
||||
@@ -459,252 +450,9 @@ public sealed class SidecarProtocolHost
|
||||
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(
|
||||
string RawCommand,
|
||||
SidecarCommandEnvelope Envelope,
|
||||
TextWriter Output,
|
||||
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]
|
||||
public async Task ValidateWorkflowCommand_ReturnsIssuesAndCompletion()
|
||||
{
|
||||
@@ -668,7 +765,7 @@ public sealed class SidecarProtocolHostTests
|
||||
[Fact]
|
||||
public void MapRuntimeTools_ExcludesOnlyInternalMetaToolsAndDeduplicatesByName()
|
||||
{
|
||||
IReadOnlyList<SidecarRuntimeToolDto> runtimeTools = SidecarProtocolHost.MapRuntimeTools(
|
||||
IReadOnlyList<SidecarRuntimeToolDto> runtimeTools = CopilotAgentProvider.MapRuntimeTools(
|
||||
[
|
||||
new Tool
|
||||
{
|
||||
@@ -721,7 +818,7 @@ public sealed class SidecarProtocolHostTests
|
||||
[Fact]
|
||||
public void ClassifyConnectionStatus_ReturnsAuthRequiredForLoginFailures()
|
||||
{
|
||||
string status = SidecarProtocolHost.ClassifyConnectionStatus(
|
||||
string status = CopilotAgentProvider.ClassifyConnectionStatus(
|
||||
new InvalidOperationException("Please run copilot auth login to continue."));
|
||||
|
||||
Assert.Equal("copilot-auth-required", status);
|
||||
@@ -731,7 +828,7 @@ public sealed class SidecarProtocolHostTests
|
||||
public void CreateReadyConnectionDiagnostics_ReportsCliPathAndModelCount()
|
||||
{
|
||||
SidecarConnectionDiagnosticsDto diagnostics =
|
||||
SidecarProtocolHost.CreateReadyConnectionDiagnostics(
|
||||
CopilotAgentProvider.CreateReadyConnectionDiagnostics(
|
||||
@"C:\tools\copilot\copilot.exe",
|
||||
2,
|
||||
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
|
||||
{
|
||||
public IReadOnlyList<CopilotSessionInfoDto> Sessions { get; init; } = [];
|
||||
|
||||
Reference in New Issue
Block a user