diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index b6650cd57..afe9b03bd 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -58,7 +58,7 @@ jobs: - name: Set up .NET uses: actions/setup-dotnet@v5 with: - dotnet-version: "8.0.x" + dotnet-version: "10.0.x" # Install just command runner - name: Install just diff --git a/.github/workflows/docs-validation.yml b/.github/workflows/docs-validation.yml index 20031af07..89d2fa2a9 100644 --- a/.github/workflows/docs-validation.yml +++ b/.github/workflows/docs-validation.yml @@ -108,7 +108,7 @@ jobs: - uses: actions/setup-dotnet@v5 with: - dotnet-version: "8.0.x" + dotnet-version: "10.0.x" - name: Install validation dependencies working-directory: scripts/docs-validation diff --git a/.github/workflows/dotnet-sdk-tests.yml b/.github/workflows/dotnet-sdk-tests.yml index bbe577bc1..3ca9d1de9 100644 --- a/.github/workflows/dotnet-sdk-tests.yml +++ b/.github/workflows/dotnet-sdk-tests.yml @@ -43,7 +43,7 @@ jobs: - uses: actions/checkout@v6.0.2 - uses: actions/setup-dotnet@v5 with: - dotnet-version: "8.0.x" + dotnet-version: "10.0.x" - uses: actions/setup-node@v6 with: node-version: "22" diff --git a/.github/workflows/nodejs-sdk-tests.yml b/.github/workflows/nodejs-sdk-tests.yml index 5947396d0..9e978a22f 100644 --- a/.github/workflows/nodejs-sdk-tests.yml +++ b/.github/workflows/nodejs-sdk-tests.yml @@ -12,6 +12,7 @@ on: - 'nodejs/**' - 'test/**' - '.github/workflows/nodejs-sdk-tests.yml' + - '!nodejs/scripts/**' - '!**/*.md' - '!**/LICENSE*' - '!**/.gitignore' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b855566a5..89df96c9f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -121,7 +121,7 @@ jobs: - uses: actions/checkout@v6.0.2 - uses: actions/setup-dotnet@v5 with: - dotnet-version: "8.0.x" + dotnet-version: "10.0.x" - name: Restore dependencies run: dotnet restore - name: Build and pack diff --git a/.github/workflows/scenario-builds.yml b/.github/workflows/scenario-builds.yml index a66ede5ec..54d7257e5 100644 --- a/.github/workflows/scenario-builds.yml +++ b/.github/workflows/scenario-builds.yml @@ -152,7 +152,7 @@ jobs: - uses: actions/setup-dotnet@v5 with: - dotnet-version: "8.0.x" + dotnet-version: "10.0.x" - uses: actions/cache@v4 with: diff --git a/.github/workflows/update-copilot-dependency.yml b/.github/workflows/update-copilot-dependency.yml index 34a6c97a2..b1d3cae6d 100644 --- a/.github/workflows/update-copilot-dependency.yml +++ b/.github/workflows/update-copilot-dependency.yml @@ -38,7 +38,7 @@ jobs: - uses: actions/setup-dotnet@v5 with: - dotnet-version: "8.0.x" + dotnet-version: "10.0.x" - name: Update @github/copilot in nodejs env: diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props new file mode 100644 index 000000000..badf8483d --- /dev/null +++ b/dotnet/Directory.Build.props @@ -0,0 +1,12 @@ + + + + net8.0 + 14 + enable + enable + 10.0-minimum + true + + + diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props new file mode 100644 index 000000000..5447fee51 --- /dev/null +++ b/dotnet/Directory.Packages.props @@ -0,0 +1,19 @@ + + + + true + + + + + + + + + + + + + + + diff --git a/dotnet/global.json b/dotnet/global.json new file mode 100644 index 000000000..c0c9c61a0 --- /dev/null +++ b/dotnet/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "major" + } +} diff --git a/dotnet/nuget.config b/dotnet/nuget.config new file mode 100644 index 000000000..128d95e59 --- /dev/null +++ b/dotnet/nuget.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dotnet/samples/Chat.csproj b/dotnet/samples/Chat.csproj index 4121ceaef..ad90a6062 100644 --- a/dotnet/samples/Chat.csproj +++ b/dotnet/samples/Chat.csproj @@ -1,9 +1,6 @@ Exe - net8.0 - enable - enable diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index cf6c5a29d..223935e7f 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -16,6 +16,7 @@ using System.Text.Json.Serialization; using System.Text.RegularExpressions; using GitHub.Copilot.SDK.Rpc; +using System.Globalization; namespace GitHub.Copilot.SDK; @@ -51,7 +52,7 @@ namespace GitHub.Copilot.SDK; /// await session.SendAsync(new MessageOptions { Prompt = "Hello!" }); /// /// -public partial class CopilotClient : IDisposable, IAsyncDisposable +public sealed partial class CopilotClient : IDisposable, IAsyncDisposable { private readonly ConcurrentDictionary _sessions = new(); private readonly CopilotClientOptions _options; @@ -62,8 +63,8 @@ public partial class CopilotClient : IDisposable, IAsyncDisposable private readonly string? _optionsHost; private List? _modelsCache; private readonly SemaphoreSlim _modelsCacheLock = new(1, 1); - private readonly List> _lifecycleHandlers = new(); - private readonly Dictionary>> _typedLifecycleHandlers = new(); + private readonly List> _lifecycleHandlers = []; + private readonly Dictionary>> _typedLifecycleHandlers = []; private readonly object _lifecycleHandlersLock = new(); private ServerRpc? _rpc; @@ -241,7 +242,7 @@ public async Task StopAsync() } catch (Exception ex) { - errors.Add(new Exception($"Failed to destroy session {session.SessionId}: {ex.Message}", ex)); + errors.Add(new IOException($"Failed to destroy session {session.SessionId}: {ex.Message}", ex)); } } @@ -611,7 +612,7 @@ public async Task> ListModelsAsync(CancellationToken cancellatio // Check cache (already inside lock) if (_modelsCache is not null) { - return new List(_modelsCache); // Return a copy to prevent cache mutation + return [.. _modelsCache]; // Return a copy to prevent cache mutation } // Cache miss - fetch from backend while holding lock @@ -621,7 +622,7 @@ public async Task> ListModelsAsync(CancellationToken cancellatio // Update cache before releasing lock _modelsCache = response.Models; - return new List(response.Models); // Return a copy to prevent cache mutation + return [.. response.Models]; // Return a copy to prevent cache mutation } finally { @@ -820,7 +821,7 @@ public IDisposable On(string eventType, Action handler) { if (!_typedLifecycleHandlers.TryGetValue(eventType, out var handlers)) { - handlers = new List>(); + handlers = []; _typedLifecycleHandlers[eventType] = handlers; } handlers.Add(handler); @@ -846,9 +847,9 @@ private void DispatchLifecycleEvent(SessionLifecycleEvent evt) lock (_lifecycleHandlersLock) { typedHandlers = _typedLifecycleHandlers.TryGetValue(evt.Type, out var handlers) - ? new List>(handlers) - : new List>(); - wildcardHandlers = new List>(_lifecycleHandlers); + ? [.. handlers] + : []; + wildcardHandlers = [.. _lifecycleHandlers]; } foreach (var handler in typedHandlers) @@ -907,7 +908,7 @@ private Task EnsureConnectedAsync(CancellationToken cancellationToke return (Task)StartAsync(cancellationToken); } - private async Task VerifyProtocolVersionAsync(Connection connection, CancellationToken cancellationToken) + private static async Task VerifyProtocolVersionAsync(Connection connection, CancellationToken cancellationToken) { var expectedVersion = SdkProtocolVersion.GetVersion(); var pingResponse = await InvokeRpcAsync( @@ -950,7 +951,7 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio } else if (options.Port > 0) { - args.AddRange(["--port", options.Port.ToString()]); + args.AddRange(["--port", options.Port.ToString(CultureInfo.InvariantCulture)]); } // Add auth-related flags @@ -1013,7 +1014,11 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio { stderrBuffer.AppendLine(line); } - logger.LogDebug("[CLI] {Line}", line); + + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogDebug("[CLI] {Line}", line); + } } } }, cancellationToken); @@ -1027,13 +1032,10 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio while (!cts.Token.IsCancellationRequested) { - var line = await cliProcess.StandardOutput.ReadLineAsync(cts.Token); - if (line == null) throw new Exception("CLI process exited unexpectedly"); - - var match = Regex.Match(line, @"listening on port (\d+)", RegexOptions.IgnoreCase); - if (match.Success) + var line = await cliProcess.StandardOutput.ReadLineAsync(cts.Token) ?? throw new IOException("CLI process exited unexpectedly"); + if (ListeningOnPortRegex().Match(line) is { Success: true } match) { - detectedLocalhostTcpPort = int.Parse(match.Groups[1].Value); + detectedLocalhostTcpPort = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); break; } } @@ -1133,8 +1135,10 @@ private async Task ConnectToServerAsync(Process? cliProcess, string? [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Using happy path from https://microsoft.github.io/vs-streamjsonrpc/docs/nativeAOT.html")] [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Using happy path from https://microsoft.github.io/vs-streamjsonrpc/docs/nativeAOT.html")] - private static SystemTextJsonFormatter CreateSystemTextJsonFormatter() => - new SystemTextJsonFormatter() { JsonSerializerOptions = SerializerOptionsForMessageFormatter }; + private static SystemTextJsonFormatter CreateSystemTextJsonFormatter() + { + return new() { JsonSerializerOptions = SerializerOptionsForMessageFormatter }; + } private static JsonSerializerOptions SerializerOptionsForMessageFormatter { get; } = CreateSerializerOptions(); @@ -1157,8 +1161,10 @@ private static JsonSerializerOptions CreateSerializerOptions() return options; } - internal CopilotSession? GetSession(string sessionId) => - _sessions.TryGetValue(sessionId, out var session) ? session : null; + internal CopilotSession? GetSession(string sessionId) + { + return _sessions.TryGetValue(sessionId, out var session) ? session : null; + } /// /// Disposes the synchronously. @@ -1168,7 +1174,7 @@ private static JsonSerializerOptions CreateSerializerOptions() /// public void Dispose() { - DisposeAsync().GetAwaiter().GetResult(); + DisposeAsync().AsTask().GetAwaiter().GetResult(); } /// @@ -1223,12 +1229,7 @@ public async Task OnToolCall(string sessionId, string toolName, object? arguments) { - var session = client.GetSession(sessionId); - if (session == null) - { - throw new ArgumentException($"Unknown session {sessionId}"); - } - + var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}"); if (session.GetTool(toolName) is not { } tool) { return new ToolCallResponse(new ToolResultObject @@ -1335,12 +1336,7 @@ public async Task OnPermissionRequest(string sessionI public async Task OnUserInputRequest(string sessionId, string question, List? choices = null, bool? allowFreeform = null) { - var session = client.GetSession(sessionId); - if (session == null) - { - throw new ArgumentException($"Unknown session {sessionId}"); - } - + var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}"); var request = new UserInputRequest { Question = question, @@ -1354,12 +1350,7 @@ public async Task OnUserInputRequest(string sessionId, public async Task OnHooksInvoke(string sessionId, string hookType, JsonElement input) { - var session = client.GetSession(sessionId); - if (session == null) - { - throw new ArgumentException($"Unknown session {sessionId}"); - } - + var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}"); var output = await session.HandleHooksInvokeAsync(hookType, input); return new HooksInvokeResponse(output); } @@ -1499,33 +1490,70 @@ public LoggerTraceSource(ILogger logger) : base(nameof(LoggerTraceSource), Sourc private sealed class LoggerTraceListener(ILogger logger) : TraceListener { - public override void TraceEvent(TraceEventCache? eventCache, string source, TraceEventType eventType, int id, string? message) => - logger.Log(MapLevel(eventType), "[{Source}] {Message}", source, message); + public override void TraceEvent(TraceEventCache? eventCache, string source, TraceEventType eventType, int id, string? message) + { + LogLevel level = MapLevel(eventType); + if (logger.IsEnabled(level)) + { + logger.Log(level, "[{Source}] {Message}", source, message); + } + } - public override void TraceEvent(TraceEventCache? eventCache, string source, TraceEventType eventType, int id, string? format, params object?[]? args) => - logger.Log(MapLevel(eventType), "[{Source}] {Message}", source, args is null || args.Length == 0 ? format : string.Format(format ?? "", args)); + public override void TraceEvent(TraceEventCache? eventCache, string source, TraceEventType eventType, int id, string? format, params object?[]? args) + { + LogLevel level = MapLevel(eventType); + if (logger.IsEnabled(level)) + { + logger.Log(level, "[{Source}] {Message}", source, args is null || args.Length == 0 ? format : string.Format(CultureInfo.InvariantCulture, format ?? "", args)); + } + } - public override void TraceData(TraceEventCache? eventCache, string source, TraceEventType eventType, int id, object? data) => - logger.Log(MapLevel(eventType), "[{Source}] {Data}", source, data); + public override void TraceData(TraceEventCache? eventCache, string source, TraceEventType eventType, int id, object? data) + { + LogLevel level = MapLevel(eventType); + if (logger.IsEnabled(level)) + { + logger.Log(level, "[{Source}] {Data}", source, data); + } + } - public override void TraceData(TraceEventCache? eventCache, string source, TraceEventType eventType, int id, params object?[]? data) => - logger.Log(MapLevel(eventType), "[{Source}] {Data}", source, data is null ? null : string.Join(", ", data)); + public override void TraceData(TraceEventCache? eventCache, string source, TraceEventType eventType, int id, params object?[]? data) + { + LogLevel level = MapLevel(eventType); + if (logger.IsEnabled(level)) + { + logger.Log(level, "[{Source}] {Data}", source, data is null ? null : string.Join(", ", data)); + } + } - public override void Write(string? message) => - logger.LogTrace("{Message}", message); + public override void Write(string? message) + { + if (logger.IsEnabled(LogLevel.Trace)) + { + logger.LogTrace("{Message}", message); + } + } - public override void WriteLine(string? message) => - logger.LogTrace("{Message}", message); + public override void WriteLine(string? message) + { + if (logger.IsEnabled(LogLevel.Trace)) + { + logger.LogTrace("{Message}", message); + } + } - private static LogLevel MapLevel(TraceEventType eventType) => eventType switch + private static LogLevel MapLevel(TraceEventType eventType) { - TraceEventType.Critical => LogLevel.Critical, - TraceEventType.Error => LogLevel.Error, - TraceEventType.Warning => LogLevel.Warning, - TraceEventType.Information => LogLevel.Information, - TraceEventType.Verbose => LogLevel.Debug, - _ => LogLevel.Trace - }; + return eventType switch + { + TraceEventType.Critical => LogLevel.Critical, + TraceEventType.Error => LogLevel.Error, + TraceEventType.Warning => LogLevel.Warning, + TraceEventType.Information => LogLevel.Information, + TraceEventType.Verbose => LogLevel.Debug, + _ => LogLevel.Trace + }; + } } } @@ -1558,11 +1586,20 @@ public override void WriteLine(string? message) => [JsonSerializable(typeof(UserInputRequest))] [JsonSerializable(typeof(UserInputResponse))] internal partial class ClientJsonContext : JsonSerializerContext; + + [GeneratedRegex(@"listening on port ([0-9]+)", RegexOptions.IgnoreCase)] + private static partial Regex ListeningOnPortRegex(); } -// Must inherit from AIContent as a signal to MEAI to avoid JSON-serializing the -// value before passing it back to us +/// +/// Wraps a as to pass structured tool results +/// back through Microsoft.Extensions.AI without JSON serialization. +/// +/// The tool result to wrap. public class ToolResultAIContent(ToolResultObject toolResult) : AIContent { + /// + /// Gets the underlying . + /// public ToolResultObject Result => toolResult; } diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 73d6e769d..4c4bac0f3 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -5,6 +5,9 @@ // AUTO-GENERATED FILE - DO NOT EDIT // Generated from: api.schema.json +// Generated code does not have XML doc comments; suppress CS1591 to avoid warnings. +#pragma warning disable CS1591 + using System.Text.Json; using System.Text.Json.Serialization; using StreamJsonRpc; @@ -116,7 +119,7 @@ public class ModelsListResult { /// List of available models with full metadata [JsonPropertyName("models")] - public List Models { get; set; } = new(); + public List Models { get; set; } = []; } public class Tool @@ -146,7 +149,7 @@ public class ToolsListResult { /// List of available built-in tools with metadata [JsonPropertyName("tools")] - public List Tools { get; set; } = new(); + public List Tools { get; set; } = []; } internal class ToolsListRequest @@ -186,7 +189,7 @@ public class AccountGetQuotaResult { /// Quota snapshots keyed by type (e.g., chat, completions, premium_interactions) [JsonPropertyName("quotaSnapshots")] - public Dictionary QuotaSnapshots { get; set; } = new(); + public Dictionary QuotaSnapshots { get; set; } = []; } public class SessionModelGetCurrentResult @@ -289,7 +292,7 @@ public class SessionWorkspaceListFilesResult { /// Relative file paths in the workspace files directory [JsonPropertyName("files")] - public List Files { get; set; } = new(); + public List Files { get; set; } = []; } internal class SessionWorkspaceListFilesRequest @@ -365,7 +368,7 @@ public class SessionAgentListResult { /// Available custom agents [JsonPropertyName("agents")] - public List Agents { get; set; } = new(); + public List Agents { get; set; } = []; } internal class SessionAgentListRequest diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 25a65f3f7..6de4d0ef3 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -5,6 +5,9 @@ // AUTO-GENERATED FILE - DO NOT EDIT // Generated from: session-events.schema.json +// Generated code does not have XML doc comments; suppress CS1591 to avoid warnings. +#pragma warning disable CS1591 + using System.Text.Json; using System.Text.Json.Serialization; diff --git a/dotnet/src/GitHub.Copilot.SDK.csproj b/dotnet/src/GitHub.Copilot.SDK.csproj index 019788cfa..8ae53ca74 100644 --- a/dotnet/src/GitHub.Copilot.SDK.csproj +++ b/dotnet/src/GitHub.Copilot.SDK.csproj @@ -1,20 +1,26 @@  - net8.0 - enable - enable - true + true 0.1.0 SDK for programmatic control of GitHub Copilot CLI GitHub GitHub Copyright (c) Microsoft Corporation. All rights reserved. MIT + https://github.com/github/copilot-sdk README.md https://github.com/github/copilot-sdk github;copilot;sdk;jsonrpc;agent true + true + snupkg + true + true + + + + true @@ -22,10 +28,11 @@ - - - - + + + + + diff --git a/dotnet/src/SdkProtocolVersion.cs b/dotnet/src/SdkProtocolVersion.cs index bb47dfebf..b4c2a367f 100644 --- a/dotnet/src/SdkProtocolVersion.cs +++ b/dotnet/src/SdkProtocolVersion.cs @@ -11,10 +11,13 @@ internal static class SdkProtocolVersion /// /// The SDK protocol version. /// - public const int Version = 2; + private const int Version = 2; /// /// Gets the SDK protocol version. /// - public static int GetVersion() => Version; + public static int GetVersion() + { + return Version; + } } diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 8bbee6071..68b02b7c0 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -42,7 +42,7 @@ namespace GitHub.Copilot.SDK; /// await session.SendAndWaitAsync(new MessageOptions { Prompt = "Hello, world!" }); /// /// -public partial class CopilotSession : IAsyncDisposable +public sealed partial class CopilotSession : IAsyncDisposable { /// /// Multicast delegate used as a thread-safe, insertion-ordered handler list. @@ -50,8 +50,8 @@ public partial class CopilotSession : IAsyncDisposable /// Dispatch reads the field once (inherent snapshot, no allocation). /// Expected handler count is small (typically 1–3), so Delegate.Combine/Remove cost is negligible. /// - private event SessionEventHandler? _eventHandlers; - private readonly Dictionary _toolHandlers = new(); + private event SessionEventHandler? EventHandlers; + private readonly Dictionary _toolHandlers = []; private readonly JsonRpc _rpc; private volatile PermissionRequestHandler? _permissionHandler; private volatile UserInputHandler? _userInputHandler; @@ -96,8 +96,10 @@ internal CopilotSession(string sessionId, JsonRpc rpc, string? workspacePath = n WorkspacePath = workspacePath; } - private Task InvokeRpcAsync(string method, object?[]? args, CancellationToken cancellationToken) => - CopilotClient.InvokeRpcAsync(_rpc, method, args, cancellationToken); + private Task InvokeRpcAsync(string method, object?[]? args, CancellationToken cancellationToken) + { + return CopilotClient.InvokeRpcAsync(_rpc, method, args, cancellationToken); + } /// /// Sends a message to the Copilot session and waits for the response. @@ -249,8 +251,8 @@ void Handler(SessionEvent evt) /// public IDisposable On(SessionEventHandler handler) { - _eventHandlers += handler; - return new ActionDisposable(() => _eventHandlers -= handler); + EventHandlers += handler; + return new ActionDisposable(() => EventHandlers -= handler); } /// @@ -263,7 +265,7 @@ public IDisposable On(SessionEventHandler handler) internal void DispatchEvent(SessionEvent sessionEvent) { // Reading the field once gives us a snapshot; delegates are immutable. - _eventHandlers?.Invoke(sessionEvent); + EventHandlers?.Invoke(sessionEvent); } /// @@ -288,8 +290,10 @@ internal void RegisterTools(ICollection tools) /// /// The name of the tool to retrieve. /// The tool if found; otherwise, null. - internal AIFunction? GetTool(string name) => - _toolHandlers.TryGetValue(name, out var tool) ? tool : null; + internal AIFunction? GetTool(string name) + { + return _toolHandlers.TryGetValue(name, out var tool) ? tool : null; + } /// /// Registers a handler for permission requests. @@ -348,13 +352,7 @@ internal void RegisterUserInputHandler(UserInputHandler handler) /// A task that resolves with the user's response. internal async Task HandleUserInputRequestAsync(UserInputRequest request) { - var handler = _userInputHandler; - - if (handler == null) - { - throw new InvalidOperationException("No user input handler registered"); - } - + var handler = _userInputHandler ?? throw new InvalidOperationException("No user input handler registered"); var invocation = new UserInputInvocation { SessionId = SessionId @@ -569,7 +567,7 @@ await InvokeRpcAsync( // Connection is broken or closed } - _eventHandlers = null; + EventHandlers = null; _toolHandlers.Clear(); _permissionHandler = null; @@ -595,7 +593,7 @@ internal record GetMessagesRequest internal record GetMessagesResponse { - public List Events { get; init; } = new(); + public List Events { get; init; } = []; } internal record SessionAbortRequest diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 97f5ebbbc..e294adf80 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using System.ComponentModel; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.AI; @@ -9,19 +10,29 @@ namespace GitHub.Copilot.SDK; -[JsonConverter(typeof(JsonStringEnumConverter))] +/// +/// Represents the connection state of the Copilot client. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] public enum ConnectionState { + /// The client is not connected to the server. [JsonStringEnumMemberName("disconnected")] Disconnected, + /// The client is establishing a connection to the server. [JsonStringEnumMemberName("connecting")] Connecting, + /// The client is connected and ready to communicate. [JsonStringEnumMemberName("connected")] Connected, + /// The connection is in an error state. [JsonStringEnumMemberName("error")] Error } +/// +/// Configuration options for creating a instance. +/// public class CopilotClientOptions { /// @@ -56,15 +67,45 @@ protected CopilotClientOptions(CopilotClientOptions? other) /// Path to the Copilot CLI executable. If not specified, uses the bundled CLI from the SDK. /// public string? CliPath { get; set; } + /// + /// Additional command-line arguments to pass to the CLI process. + /// public string[]? CliArgs { get; set; } + /// + /// Working directory for the CLI process. + /// public string? Cwd { get; set; } + /// + /// Port number for the CLI server when not using stdio transport. + /// public int Port { get; set; } + /// + /// Whether to use stdio transport for communication with the CLI server. + /// public bool UseStdio { get; set; } = true; + /// + /// URL of an existing CLI server to connect to instead of starting a new one. + /// public string? CliUrl { get; set; } + /// + /// Log level for the CLI server (e.g., "info", "debug", "warn", "error"). + /// public string LogLevel { get; set; } = "info"; + /// + /// Whether to automatically start the CLI server if it is not already running. + /// public bool AutoStart { get; set; } = true; + /// + /// Whether to automatically restart the CLI server if it exits unexpectedly. + /// public bool AutoRestart { get; set; } = true; + /// + /// Environment variables to pass to the CLI process. + /// public IReadOnlyDictionary? Environment { get; set; } + /// + /// Logger instance for SDK diagnostic output. + /// public ILogger? Logger { get; set; } /// @@ -77,6 +118,7 @@ protected CopilotClientOptions(CopilotClientOptions? other) /// /// Obsolete. Use instead. /// + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Use GitHubToken instead.", error: false)] public string? GithubToken { @@ -101,81 +143,188 @@ public string? GithubToken /// Other reference-type properties (for example delegates and the logger) are not /// deep-cloned; the original and the clone will share those objects. /// - public virtual CopilotClientOptions Clone() => new(this); + public virtual CopilotClientOptions Clone() + { + return new(this); + } } +/// +/// Represents a binary result returned by a tool invocation. +/// public class ToolBinaryResult { + /// + /// Base64-encoded binary data. + /// [JsonPropertyName("data")] public string Data { get; set; } = string.Empty; + /// + /// MIME type of the binary data (e.g., "image/png"). + /// [JsonPropertyName("mimeType")] public string MimeType { get; set; } = string.Empty; + /// + /// Type identifier for the binary result. + /// [JsonPropertyName("type")] public string Type { get; set; } = string.Empty; + /// + /// Optional human-readable description of the binary result. + /// [JsonPropertyName("description")] public string? Description { get; set; } } +/// +/// Represents the structured result of a tool execution. +/// public class ToolResultObject { + /// + /// Text result to be consumed by the language model. + /// [JsonPropertyName("textResultForLlm")] public string TextResultForLlm { get; set; } = string.Empty; + /// + /// Binary results (e.g., images) to be consumed by the language model. + /// [JsonPropertyName("binaryResultsForLlm")] public List? BinaryResultsForLlm { get; set; } + /// + /// Result type indicator. + /// + /// "success" — the tool executed successfully. + /// "failure" — the tool encountered an error. + /// "rejected" — the tool invocation was rejected. + /// "denied" — the tool invocation was denied by a permission check. + /// + /// [JsonPropertyName("resultType")] public string ResultType { get; set; } = "success"; + /// + /// Error message if the tool execution failed. + /// [JsonPropertyName("error")] public string? Error { get; set; } + /// + /// Log entry for the session history. + /// [JsonPropertyName("sessionLog")] public string? SessionLog { get; set; } + /// + /// Custom telemetry data associated with the tool execution. + /// [JsonPropertyName("toolTelemetry")] public Dictionary? ToolTelemetry { get; set; } } +/// +/// Contains context for a tool invocation callback. +/// public class ToolInvocation { + /// + /// Identifier of the session that triggered the tool call. + /// public string SessionId { get; set; } = string.Empty; + /// + /// Unique identifier of this specific tool call. + /// public string ToolCallId { get; set; } = string.Empty; + /// + /// Name of the tool being invoked. + /// public string ToolName { get; set; } = string.Empty; + /// + /// Arguments passed to the tool by the language model. + /// public object? Arguments { get; set; } } +/// +/// Delegate for handling tool invocations and returning a result. +/// public delegate Task ToolHandler(ToolInvocation invocation); +/// +/// Represents a permission request from the server for a tool operation. +/// public class PermissionRequest { + /// + /// Kind of permission being requested. + /// + /// "shell" — execute a shell command. + /// "write" — write to a file. + /// "read" — read a file. + /// "mcp" — invoke an MCP server tool. + /// "url" — access a URL. + /// "custom-tool" — invoke a custom tool. + /// + /// [JsonPropertyName("kind")] public string Kind { get; set; } = string.Empty; + /// + /// Identifier of the tool call that triggered the permission request. + /// [JsonPropertyName("toolCallId")] public string? ToolCallId { get; set; } + /// + /// Additional properties not explicitly modeled. + /// [JsonExtensionData] public Dictionary? ExtensionData { get; set; } } +/// +/// Result of a permission request evaluation. +/// public class PermissionRequestResult { + /// + /// Permission decision kind. + /// + /// "approved" — the operation is allowed. + /// "denied-by-rules" — denied by configured permission rules. + /// "denied-interactively-by-user" — the user explicitly denied the request. + /// "denied-no-approval-rule-and-could-not-request-from-user" — no rule matched and user approval was unavailable. + /// + /// [JsonPropertyName("kind")] public string Kind { get; set; } = string.Empty; + /// + /// Permission rules to apply for the decision. + /// [JsonPropertyName("rules")] public List? Rules { get; set; } } +/// +/// Contains context for a permission request callback. +/// public class PermissionInvocation { + /// + /// Identifier of the session that triggered the permission request. + /// public string SessionId { get; set; } = string.Empty; } +/// +/// Delegate for handling permission requests and returning a decision. +/// public delegate Task PermissionRequestHandler(PermissionRequest request, PermissionInvocation invocation); // ============================================================================ @@ -229,6 +378,9 @@ public class UserInputResponse /// public class UserInputInvocation { + /// + /// Identifier of the session that triggered the user input request. + /// public string SessionId { get; set; } = string.Empty; } @@ -246,6 +398,9 @@ public class UserInputInvocation /// public class HookInvocation { + /// + /// Identifier of the session that triggered the hook. + /// public string SessionId { get; set; } = string.Empty; } @@ -254,15 +409,27 @@ public class HookInvocation /// public class PreToolUseHookInput { + /// + /// Unix timestamp in milliseconds when the tool use was initiated. + /// [JsonPropertyName("timestamp")] public long Timestamp { get; set; } + /// + /// Current working directory of the session. + /// [JsonPropertyName("cwd")] public string Cwd { get; set; } = string.Empty; + /// + /// Name of the tool about to be executed. + /// [JsonPropertyName("toolName")] public string ToolName { get; set; } = string.Empty; + /// + /// Arguments that will be passed to the tool. + /// [JsonPropertyName("toolArgs")] public object? ToolArgs { get; set; } } @@ -273,24 +440,44 @@ public class PreToolUseHookInput public class PreToolUseHookOutput { /// - /// Permission decision: "allow", "deny", or "ask". + /// Permission decision for the pending tool call. + /// + /// "allow" — permit the tool to execute. + /// "deny" — block the tool from executing. + /// "ask" — fall through to the normal permission prompt. + /// /// [JsonPropertyName("permissionDecision")] public string? PermissionDecision { get; set; } + /// + /// Human-readable reason for the permission decision. + /// [JsonPropertyName("permissionDecisionReason")] public string? PermissionDecisionReason { get; set; } + /// + /// Modified arguments to pass to the tool instead of the original ones. + /// [JsonPropertyName("modifiedArgs")] public object? ModifiedArgs { get; set; } + /// + /// Additional context to inject into the conversation for the language model. + /// [JsonPropertyName("additionalContext")] public string? AdditionalContext { get; set; } + /// + /// Whether to suppress the tool's output from the conversation. + /// [JsonPropertyName("suppressOutput")] public bool? SuppressOutput { get; set; } } +/// +/// Delegate invoked before a tool is executed, allowing modification or denial of the call. +/// public delegate Task PreToolUseHandler(PreToolUseHookInput input, HookInvocation invocation); /// @@ -298,18 +485,33 @@ public class PreToolUseHookOutput /// public class PostToolUseHookInput { + /// + /// Unix timestamp in milliseconds when the tool execution completed. + /// [JsonPropertyName("timestamp")] public long Timestamp { get; set; } + /// + /// Current working directory of the session. + /// [JsonPropertyName("cwd")] public string Cwd { get; set; } = string.Empty; + /// + /// Name of the tool that was executed. + /// [JsonPropertyName("toolName")] public string ToolName { get; set; } = string.Empty; + /// + /// Arguments that were passed to the tool. + /// [JsonPropertyName("toolArgs")] public object? ToolArgs { get; set; } + /// + /// Result returned by the tool execution. + /// [JsonPropertyName("toolResult")] public object? ToolResult { get; set; } } @@ -319,16 +521,28 @@ public class PostToolUseHookInput /// public class PostToolUseHookOutput { + /// + /// Modified result to replace the original tool result. + /// [JsonPropertyName("modifiedResult")] public object? ModifiedResult { get; set; } + /// + /// Additional context to inject into the conversation for the language model. + /// [JsonPropertyName("additionalContext")] public string? AdditionalContext { get; set; } + /// + /// Whether to suppress the tool's output from the conversation. + /// [JsonPropertyName("suppressOutput")] public bool? SuppressOutput { get; set; } } +/// +/// Delegate invoked after a tool has been executed, allowing modification of the result. +/// public delegate Task PostToolUseHandler(PostToolUseHookInput input, HookInvocation invocation); /// @@ -336,12 +550,21 @@ public class PostToolUseHookOutput /// public class UserPromptSubmittedHookInput { + /// + /// Unix timestamp in milliseconds when the prompt was submitted. + /// [JsonPropertyName("timestamp")] public long Timestamp { get; set; } + /// + /// Current working directory of the session. + /// [JsonPropertyName("cwd")] public string Cwd { get; set; } = string.Empty; + /// + /// The user's prompt text. + /// [JsonPropertyName("prompt")] public string Prompt { get; set; } = string.Empty; } @@ -351,16 +574,28 @@ public class UserPromptSubmittedHookInput /// public class UserPromptSubmittedHookOutput { + /// + /// Modified prompt to use instead of the original user prompt. + /// [JsonPropertyName("modifiedPrompt")] public string? ModifiedPrompt { get; set; } + /// + /// Additional context to inject into the conversation for the language model. + /// [JsonPropertyName("additionalContext")] public string? AdditionalContext { get; set; } + /// + /// Whether to suppress the prompt's output from the conversation. + /// [JsonPropertyName("suppressOutput")] public bool? SuppressOutput { get; set; } } +/// +/// Delegate invoked when the user submits a prompt, allowing modification of the prompt. +/// public delegate Task UserPromptSubmittedHandler(UserPromptSubmittedHookInput input, HookInvocation invocation); /// @@ -368,18 +603,32 @@ public class UserPromptSubmittedHookOutput /// public class SessionStartHookInput { + /// + /// Unix timestamp in milliseconds when the session started. + /// [JsonPropertyName("timestamp")] public long Timestamp { get; set; } + /// + /// Current working directory of the session. + /// [JsonPropertyName("cwd")] public string Cwd { get; set; } = string.Empty; /// - /// Source of the session start: "startup", "resume", or "new". + /// Source of the session start. + /// + /// "startup" — initial application startup. + /// "resume" — resuming a previous session. + /// "new" — starting a brand new session. + /// /// [JsonPropertyName("source")] public string Source { get; set; } = string.Empty; + /// + /// Initial prompt provided when the session was started. + /// [JsonPropertyName("initialPrompt")] public string? InitialPrompt { get; set; } } @@ -389,13 +638,22 @@ public class SessionStartHookInput /// public class SessionStartHookOutput { + /// + /// Additional context to inject into the session for the language model. + /// [JsonPropertyName("additionalContext")] public string? AdditionalContext { get; set; } + /// + /// Modified session configuration to apply at startup. + /// [JsonPropertyName("modifiedConfig")] public Dictionary? ModifiedConfig { get; set; } } +/// +/// Delegate invoked when a session starts, allowing injection of context or config changes. +/// public delegate Task SessionStartHandler(SessionStartHookInput input, HookInvocation invocation); /// @@ -403,21 +661,40 @@ public class SessionStartHookOutput /// public class SessionEndHookInput { + /// + /// Unix timestamp in milliseconds when the session ended. + /// [JsonPropertyName("timestamp")] public long Timestamp { get; set; } + /// + /// Current working directory of the session. + /// [JsonPropertyName("cwd")] public string Cwd { get; set; } = string.Empty; /// - /// Reason for session end: "complete", "error", "abort", "timeout", or "user_exit". + /// Reason for session end. + /// + /// "complete" — the session finished normally. + /// "error" — the session ended due to an error. + /// "abort" — the session was aborted. + /// "timeout" — the session timed out. + /// "user_exit" — the user exited the session. + /// /// [JsonPropertyName("reason")] public string Reason { get; set; } = string.Empty; + /// + /// Final message from the assistant before the session ended. + /// [JsonPropertyName("finalMessage")] public string? FinalMessage { get; set; } + /// + /// Error message if the session ended due to an error. + /// [JsonPropertyName("error")] public string? Error { get; set; } } @@ -427,16 +704,28 @@ public class SessionEndHookInput /// public class SessionEndHookOutput { + /// + /// Whether to suppress the session end output from the conversation. + /// [JsonPropertyName("suppressOutput")] public bool? SuppressOutput { get; set; } + /// + /// List of cleanup action identifiers to execute after the session ends. + /// [JsonPropertyName("cleanupActions")] public List? CleanupActions { get; set; } + /// + /// Summary of the session to persist for future reference. + /// [JsonPropertyName("sessionSummary")] public string? SessionSummary { get; set; } } +/// +/// Delegate invoked when a session ends, allowing cleanup actions or summary generation. +/// public delegate Task SessionEndHandler(SessionEndHookInput input, HookInvocation invocation); /// @@ -444,21 +733,39 @@ public class SessionEndHookOutput /// public class ErrorOccurredHookInput { + /// + /// Unix timestamp in milliseconds when the error occurred. + /// [JsonPropertyName("timestamp")] public long Timestamp { get; set; } + /// + /// Current working directory of the session. + /// [JsonPropertyName("cwd")] public string Cwd { get; set; } = string.Empty; + /// + /// Error message describing what went wrong. + /// [JsonPropertyName("error")] public string Error { get; set; } = string.Empty; /// - /// Context of the error: "model_call", "tool_execution", "system", or "user_input". + /// Context of the error. + /// + /// "model_call" — error during a model API call. + /// "tool_execution" — error during tool execution. + /// "system" — internal system error. + /// "user_input" — error processing user input. + /// /// [JsonPropertyName("errorContext")] public string ErrorContext { get; set; } = string.Empty; + /// + /// Whether the error is recoverable and the session can continue. + /// [JsonPropertyName("recoverable")] public bool Recoverable { get; set; } } @@ -468,22 +775,39 @@ public class ErrorOccurredHookInput /// public class ErrorOccurredHookOutput { + /// + /// Whether to suppress the error output from the conversation. + /// [JsonPropertyName("suppressOutput")] public bool? SuppressOutput { get; set; } /// - /// Error handling strategy: "retry", "skip", or "abort". + /// Error handling strategy. + /// + /// "retry" — retry the failed operation. + /// "skip" — skip the failed operation and continue. + /// "abort" — abort the session. + /// /// [JsonPropertyName("errorHandling")] public string? ErrorHandling { get; set; } + /// + /// Number of times to retry the failed operation. + /// [JsonPropertyName("retryCount")] public int? RetryCount { get; set; } + /// + /// Message to display to the user about the error. + /// [JsonPropertyName("userNotification")] public string? UserNotification { get; set; } } +/// +/// Delegate invoked when an error occurs, allowing custom error handling strategies. +/// public delegate Task ErrorOccurredHandler(ErrorOccurredHookInput input, HookInvocation invocation); /// @@ -522,32 +846,61 @@ public class SessionHooks public ErrorOccurredHandler? OnErrorOccurred { get; set; } } +/// +/// Specifies how a custom system message is applied to the session. +/// [JsonConverter(typeof(JsonStringEnumConverter))] public enum SystemMessageMode { + /// Append the custom system message to the default system message. [JsonStringEnumMemberName("append")] Append, + /// Replace the default system message entirely. [JsonStringEnumMemberName("replace")] Replace } +/// +/// Configuration for the system message used in a session. +/// public class SystemMessageConfig { + /// + /// How the system message is applied (append or replace). + /// public SystemMessageMode? Mode { get; set; } + /// + /// Content of the system message. + /// public string? Content { get; set; } } +/// +/// Configuration for a custom model provider. +/// public class ProviderConfig { + /// + /// Provider type identifier (e.g., "openai", "azure"). + /// [JsonPropertyName("type")] public string? Type { get; set; } + /// + /// Wire API format to use (e.g., "chat-completions"). + /// [JsonPropertyName("wireApi")] public string? WireApi { get; set; } + /// + /// Base URL of the provider's API endpoint. + /// [JsonPropertyName("baseUrl")] public string BaseUrl { get; set; } = string.Empty; + /// + /// API key for authenticating with the provider. + /// [JsonPropertyName("apiKey")] public string? ApiKey { get; set; } @@ -559,12 +912,21 @@ public class ProviderConfig [JsonPropertyName("bearerToken")] public string? BearerToken { get; set; } + /// + /// Azure-specific configuration options. + /// [JsonPropertyName("azure")] public AzureOptions? Azure { get; set; } } +/// +/// Azure OpenAI-specific provider options. +/// public class AzureOptions { + /// + /// Azure OpenAI API version to use (e.g., "2024-02-01"). + /// [JsonPropertyName("apiVersion")] public string? ApiVersion { get; set; } } @@ -582,7 +944,7 @@ public class McpLocalServerConfig /// List of tools to include from this server. Empty list means none. Use "*" for all. /// [JsonPropertyName("tools")] - public List Tools { get; set; } = new(); + public List Tools { get; set; } = []; /// /// Server type. Defaults to "local". @@ -606,7 +968,7 @@ public class McpLocalServerConfig /// Arguments to pass to the command. /// [JsonPropertyName("args")] - public List Args { get; set; } = new(); + public List Args { get; set; } = []; /// /// Environment variables to pass to the server. @@ -630,7 +992,7 @@ public class McpRemoteServerConfig /// List of tools to include from this server. Empty list means none. Use "*" for all. /// [JsonPropertyName("tools")] - public List Tools { get; set; } = new(); + public List Tools { get; set; } = []; /// /// Server type. Must be "http" or "sse". @@ -739,6 +1101,9 @@ public class InfiniteSessionConfig public double? BufferExhaustionThreshold { get; set; } } +/// +/// Configuration options for creating a new Copilot session. +/// public class SessionConfig { /// @@ -778,6 +1143,9 @@ protected SessionConfig(SessionConfig? other) WorkingDirectory = other.WorkingDirectory; } + /// + /// Optional session identifier; a new ID is generated if not provided. + /// public string? SessionId { get; set; } /// @@ -786,6 +1154,9 @@ protected SessionConfig(SessionConfig? other) /// public string? ClientName { get; set; } + /// + /// Model identifier to use for this session (e.g., "gpt-4o"). + /// public string? Model { get; set; } /// @@ -801,11 +1172,25 @@ protected SessionConfig(SessionConfig? other) /// public string? ConfigDir { get; set; } + /// + /// Custom tool functions available to the language model during the session. + /// public ICollection? Tools { get; set; } - + /// + /// System message configuration for the session. + /// public SystemMessageConfig? SystemMessage { get; set; } + /// + /// List of tool names to allow; only these tools will be available when specified. + /// public List? AvailableTools { get; set; } + /// + /// List of tool names to exclude from the session. + /// public List? ExcludedTools { get; set; } + /// + /// Custom model provider configuration for the session. + /// public ProviderConfig? Provider { get; set; } /// @@ -874,9 +1259,15 @@ protected SessionConfig(SessionConfig? other) /// hooks, infinite session configuration, and delegates) are not deep-cloned; the original /// and the clone will share those nested objects, and changes to them may affect both. /// - public virtual SessionConfig Clone() => new(this); + public virtual SessionConfig Clone() + { + return new(this); + } } +/// +/// Configuration options for resuming an existing Copilot session. +/// public class ResumeSessionConfig { /// @@ -927,6 +1318,9 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) /// public string? Model { get; set; } + /// + /// Custom tool functions available to the language model during the resumed session. + /// public ICollection? Tools { get; set; } /// @@ -946,6 +1340,9 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) /// public List? ExcludedTools { get; set; } + /// + /// Custom model provider configuration for the resumed session. + /// public ProviderConfig? Provider { get; set; } /// @@ -1030,9 +1427,15 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) /// hooks, infinite session configuration, and delegates) are not deep-cloned; the original /// and the clone will share those nested objects, and changes to them may affect both. /// - public virtual ResumeSessionConfig Clone() => new(this); + public virtual ResumeSessionConfig Clone() + { + return new(this); + } } +/// +/// Options for sending a message in a Copilot session. +/// public class MessageOptions { /// @@ -1053,8 +1456,17 @@ protected MessageOptions(MessageOptions? other) Prompt = other.Prompt; } + /// + /// The prompt text to send to the assistant. + /// public string Prompt { get; set; } = string.Empty; + /// + /// File or data attachments to include with the message. + /// public List? Attachments { get; set; } + /// + /// Interaction mode for the message (e.g., "plan", "edit"). + /// public string? Mode { get; set; } /// @@ -1066,9 +1478,15 @@ protected MessageOptions(MessageOptions? other) /// Other reference-type properties (for example attachment items) are not deep-cloned; /// the original and the clone will share those nested objects. /// - public virtual MessageOptions Clone() => new(this); + public virtual MessageOptions Clone() + { + return new(this); + } } +/// +/// Delegate for handling session events emitted during a Copilot session. +/// public delegate void SessionEventHandler(SessionEvent sessionEvent); /// @@ -1101,12 +1519,30 @@ public class SessionListFilter public string? Branch { get; set; } } +/// +/// Metadata describing a Copilot session. +/// public class SessionMetadata { + /// + /// Unique identifier of the session. + /// public string SessionId { get; set; } = string.Empty; + /// + /// Time when the session was created. + /// public DateTime StartTime { get; set; } + /// + /// Time when the session was last modified. + /// public DateTime ModifiedTime { get; set; } + /// + /// Human-readable summary of the session. + /// public string? Summary { get; set; } + /// + /// Whether the session is running on a remote server. + /// public bool IsRemote { get; set; } /// Working directory context (cwd, git info) from session creation. public SessionContext? Context { get; set; } @@ -1117,10 +1553,22 @@ internal class PingRequest public string? Message { get; set; } } +/// +/// Response from a server ping request. +/// public class PingResponse { + /// + /// Echo of the ping message. + /// public string Message { get; set; } = string.Empty; + /// + /// Server timestamp when the ping was processed. + /// public long Timestamp { get; set; } + /// + /// Protocol version supported by the server. + /// public int? ProtocolVersion { get; set; } } @@ -1147,7 +1595,17 @@ public class GetAuthStatusResponse [JsonPropertyName("isAuthenticated")] public bool IsAuthenticated { get; set; } - /// Authentication type (user, env, gh-cli, hmac, api-key, token) + /// + /// Authentication type. + /// + /// "user" — authenticated via user login. + /// "env" — authenticated via environment variable. + /// "gh-cli" — authenticated via the GitHub CLI. + /// "hmac" — authenticated via HMAC signature. + /// "api-key" — authenticated via API key. + /// "token" — authenticated via explicit token. + /// + /// [JsonPropertyName("authType")] public string? AuthType { get; set; } @@ -1169,12 +1627,21 @@ public class GetAuthStatusResponse /// public class ModelVisionLimits { + /// + /// List of supported image MIME types (e.g., "image/png", "image/jpeg"). + /// [JsonPropertyName("supported_media_types")] - public List SupportedMediaTypes { get; set; } = new(); + public List SupportedMediaTypes { get; set; } = []; + /// + /// Maximum number of images allowed in a single prompt. + /// [JsonPropertyName("max_prompt_images")] public int MaxPromptImages { get; set; } + /// + /// Maximum size in bytes for a single prompt image. + /// [JsonPropertyName("max_prompt_image_size")] public int MaxPromptImageSize { get; set; } } @@ -1184,12 +1651,21 @@ public class ModelVisionLimits /// public class ModelLimits { + /// + /// Maximum number of tokens allowed in the prompt. + /// [JsonPropertyName("max_prompt_tokens")] public int? MaxPromptTokens { get; set; } + /// + /// Maximum total tokens in the context window. + /// [JsonPropertyName("max_context_window_tokens")] public int MaxContextWindowTokens { get; set; } + /// + /// Vision-specific limits for the model. + /// [JsonPropertyName("vision")] public ModelVisionLimits? Vision { get; set; } } @@ -1199,6 +1675,9 @@ public class ModelLimits /// public class ModelSupports { + /// + /// Whether this model supports image/vision inputs. + /// [JsonPropertyName("vision")] public bool Vision { get; set; } @@ -1214,9 +1693,15 @@ public class ModelSupports /// public class ModelCapabilities { + /// + /// Feature support flags for the model. + /// [JsonPropertyName("supports")] public ModelSupports Supports { get; set; } = new(); + /// + /// Token and resource limits for the model. + /// [JsonPropertyName("limits")] public ModelLimits Limits { get; set; } = new(); } @@ -1226,9 +1711,15 @@ public class ModelCapabilities /// public class ModelPolicy { + /// + /// Policy state of the model (e.g., "enabled", "disabled"). + /// [JsonPropertyName("state")] public string State { get; set; } = string.Empty; + /// + /// Terms or conditions associated with using the model. + /// [JsonPropertyName("terms")] public string Terms { get; set; } = string.Empty; } @@ -1238,6 +1729,9 @@ public class ModelPolicy /// public class ModelBilling { + /// + /// Billing cost multiplier relative to the base model rate. + /// [JsonPropertyName("multiplier")] public double Multiplier { get; set; } } @@ -1281,8 +1775,11 @@ public class ModelInfo /// public class GetModelsResponse { + /// + /// List of available models. + /// [JsonPropertyName("models")] - public List Models { get; set; } = new(); + public List Models { get; set; } = []; } // ============================================================================ @@ -1294,10 +1791,15 @@ public class GetModelsResponse /// public static class SessionLifecycleEventTypes { + /// A new session was created. public const string Created = "session.created"; + /// A session was deleted. public const string Deleted = "session.deleted"; + /// A session was updated. public const string Updated = "session.updated"; + /// A session was brought to the foreground. public const string Foreground = "session.foreground"; + /// A session was moved to the background. public const string Background = "session.background"; } @@ -1306,12 +1808,21 @@ public static class SessionLifecycleEventTypes /// public class SessionLifecycleEventMetadata { + /// + /// ISO 8601 timestamp when the session was created. + /// [JsonPropertyName("startTime")] public string StartTime { get; set; } = string.Empty; + /// + /// ISO 8601 timestamp when the session was last modified. + /// [JsonPropertyName("modifiedTime")] public string ModifiedTime { get; set; } = string.Empty; + /// + /// Human-readable summary of the session. + /// [JsonPropertyName("summary")] public string? Summary { get; set; } } @@ -1321,12 +1832,21 @@ public class SessionLifecycleEventMetadata /// public class SessionLifecycleEvent { + /// + /// Type of lifecycle event (see ). + /// [JsonPropertyName("type")] public string Type { get; set; } = string.Empty; + /// + /// Identifier of the session this event pertains to. + /// [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + /// + /// Metadata associated with the session lifecycle event. + /// [JsonPropertyName("metadata")] public SessionLifecycleEventMetadata? Metadata { get; set; } } @@ -1336,9 +1856,15 @@ public class SessionLifecycleEvent /// public class GetForegroundSessionResponse { + /// + /// Identifier of the current foreground session, or null if none. + /// [JsonPropertyName("sessionId")] public string? SessionId { get; set; } + /// + /// Workspace path associated with the foreground session. + /// [JsonPropertyName("workspacePath")] public string? WorkspacePath { get; set; } } @@ -1348,9 +1874,15 @@ public class GetForegroundSessionResponse /// public class SetForegroundSessionResponse { + /// + /// Whether the foreground session was set successfully. + /// [JsonPropertyName("success")] public bool Success { get; set; } + /// + /// Error message if the operation failed. + /// [JsonPropertyName("error")] public string? Error { get; set; } } diff --git a/dotnet/test/ClientTests.cs b/dotnet/test/ClientTests.cs index 91b7f9241..3c3f3bdaa 100644 --- a/dotnet/test/ClientTests.cs +++ b/dotnet/test/ClientTests.cs @@ -230,14 +230,11 @@ public async Task Should_Report_Error_With_Stderr_When_CLI_Fails_To_Start() { var client = new CopilotClient(new CopilotClientOptions { - CliArgs = new[] { "--nonexistent-flag-for-testing" }, + CliArgs = ["--nonexistent-flag-for-testing"], UseStdio = true }); - var ex = await Assert.ThrowsAsync(async () => - { - await client.StartAsync(); - }); + var ex = await Assert.ThrowsAsync(() => client.StartAsync()); var errorMessage = ex.Message; // Verify we get the stderr output in the error message @@ -261,10 +258,7 @@ public async Task Should_Throw_When_CreateSession_Called_Without_PermissionHandl { using var client = new CopilotClient(new CopilotClientOptions()); - var ex = await Assert.ThrowsAsync(async () => - { - await client.CreateSessionAsync(new SessionConfig()); - }); + var ex = await Assert.ThrowsAsync(() => client.CreateSessionAsync(new SessionConfig())); Assert.Contains("OnPermissionRequest", ex.Message); Assert.Contains("is required", ex.Message); @@ -275,10 +269,7 @@ public async Task Should_Throw_When_ResumeSession_Called_Without_PermissionHandl { using var client = new CopilotClient(new CopilotClientOptions()); - var ex = await Assert.ThrowsAsync(async () => - { - await client.ResumeSessionAsync("some-session-id", new ResumeSessionConfig()); - }); + var ex = await Assert.ThrowsAsync(() => client.ResumeSessionAsync("some-session-id", new())); Assert.Contains("OnPermissionRequest", ex.Message); Assert.Contains("is required", ex.Message); diff --git a/dotnet/test/GitHub.Copilot.SDK.Test.csproj b/dotnet/test/GitHub.Copilot.SDK.Test.csproj index 654a988a0..fbc9f17c3 100644 --- a/dotnet/test/GitHub.Copilot.SDK.Test.csproj +++ b/dotnet/test/GitHub.Copilot.SDK.Test.csproj @@ -1,10 +1,6 @@ - net8.0 - enable - enable - true false @@ -19,17 +15,17 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/dotnet/test/Harness/CapiProxy.cs b/dotnet/test/Harness/CapiProxy.cs index a03502979..e6208f251 100644 --- a/dotnet/test/Harness/CapiProxy.cs +++ b/dotnet/test/Harness/CapiProxy.cs @@ -12,7 +12,7 @@ namespace GitHub.Copilot.SDK.Test.Harness; -public partial class CapiProxy : IAsyncDisposable +public sealed partial class CapiProxy : IAsyncDisposable { private Process? _process; private Task? _startupTask; @@ -129,10 +129,13 @@ public async Task> GetExchangesAsync() using var client = new HttpClient(); return await client.GetFromJsonAsync($"{url}/exchanges", CapiProxyJsonContext.Default.ListParsedHttpExchange) - ?? new List(); + ?? []; } - public async ValueTask DisposeAsync() => await StopAsync(); + public async ValueTask DisposeAsync() + { + await StopAsync(); + } private static string FindRepoRoot() { diff --git a/dotnet/test/Harness/E2ETestBase.cs b/dotnet/test/Harness/E2ETestBase.cs index dc1fa465d..e982090cb 100644 --- a/dotnet/test/Harness/E2ETestBase.cs +++ b/dotnet/test/Harness/E2ETestBase.cs @@ -40,7 +40,10 @@ public async Task InitializeAsync() await Ctx.ConfigureForTestAsync(_snapshotCategory, _testName); } - public Task DisposeAsync() => Task.CompletedTask; + public Task DisposeAsync() + { + return Task.CompletedTask; + } /// /// Creates a session with a default config that approves all permissions. @@ -64,9 +67,13 @@ protected Task ResumeSessionAsync(string sessionId, ResumeSessio return Client.ResumeSessionAsync(sessionId, config); } - protected static string GetSystemMessage(ParsedHttpExchange exchange) => - exchange.Request.Messages.FirstOrDefault(m => m.Role == "system")?.Content ?? string.Empty; + protected static string GetSystemMessage(ParsedHttpExchange exchange) + { + return exchange.Request.Messages.FirstOrDefault(m => m.Role == "system")?.Content ?? string.Empty; + } - protected static List GetToolNames(ParsedHttpExchange exchange) => - exchange.Request.Tools?.Select(t => t.Function.Name).ToList() ?? new(); + protected static List GetToolNames(ParsedHttpExchange exchange) + { + return exchange.Request.Tools?.Select(t => t.Function.Name).ToList() ?? []; + } } diff --git a/dotnet/test/Harness/E2ETestContext.cs b/dotnet/test/Harness/E2ETestContext.cs index 00fc32075..a4472e1d3 100644 --- a/dotnet/test/Harness/E2ETestContext.cs +++ b/dotnet/test/Harness/E2ETestContext.cs @@ -7,7 +7,7 @@ namespace GitHub.Copilot.SDK.Test.Harness; -public class E2ETestContext : IAsyncDisposable +public sealed class E2ETestContext : IAsyncDisposable { public string HomeDir { get; } public string WorkDir { get; } @@ -74,7 +74,10 @@ public async Task ConfigureForTestAsync(string testFile, [CallerMemberName] stri await _proxy.ConfigureAsync(snapshotPath, WorkDir); } - public Task> GetExchangesAsync() => _proxy.GetExchangesAsync(); + public Task> GetExchangesAsync() + { + return _proxy.GetExchangesAsync(); + } public IReadOnlyDictionary GetEnvironment() { @@ -89,13 +92,16 @@ public IReadOnlyDictionary GetEnvironment() return env!; } - public CopilotClient CreateClient() => new(new CopilotClientOptions + public CopilotClient CreateClient() { - Cwd = WorkDir, - CliPath = GetCliPath(_repoRoot), - Environment = GetEnvironment(), - GitHubToken = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")) ? "fake-token-for-e2e-tests" : null, - }); + return new(new CopilotClientOptions + { + Cwd = WorkDir, + CliPath = GetCliPath(_repoRoot), + Environment = GetEnvironment(), + GitHubToken = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")) ? "fake-token-for-e2e-tests" : null, + }); + } public async ValueTask DisposeAsync() { diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index eac00b06e..442cbfb0d 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -95,7 +95,7 @@ public async Task Should_Create_A_Session_With_AvailableTools() { var session = await CreateSessionAsync(new SessionConfig { - AvailableTools = new List { "view", "edit" } + AvailableTools = ["view", "edit"] }); await session.SendAsync(new MessageOptions { Prompt = "What is 1+1?" }); @@ -115,7 +115,7 @@ public async Task Should_Create_A_Session_With_ExcludedTools() { var session = await CreateSessionAsync(new SessionConfig { - ExcludedTools = new List { "view" } + ExcludedTools = ["view"] }); await session.SendAsync(new MessageOptions { Prompt = "What is 1+1?" }); diff --git a/dotnet/test/ToolsTests.cs b/dotnet/test/ToolsTests.cs index 1bb6b2cb2..469645ba0 100644 --- a/dotnet/test/ToolsTests.cs +++ b/dotnet/test/ToolsTests.cs @@ -137,7 +137,7 @@ await session.SendAsync(new MessageOptions City[] PerformDbQuery(DbQueryOptions query, AIFunctionArguments rawArgs) { Assert.Equal("cities", query.Table); - Assert.Equal(new[] { 12, 19 }, query.Ids); + Assert.Equal([12, 19], query.Ids); Assert.True(query.SortAscending); receivedInvocation = (ToolInvocation)rawArgs.Context![typeof(ToolInvocation)]!; return [new(19, "Passos", 135460), new(12, "San Lorenzo", 204356)]; @@ -200,7 +200,7 @@ await session.SendAsync(new MessageOptions Assert.Contains("yellow", assistantMessage!.Data.Content?.ToLowerInvariant() ?? string.Empty); - static ToolResultAIContent GetImage() => new ToolResultAIContent(new() + static ToolResultAIContent GetImage() => new(new() { BinaryResultsForLlm = [new() { // 2x2 yellow square @@ -256,10 +256,7 @@ public async Task Denies_Custom_Tool_When_Permission_Denied() var session = await Client.CreateSessionAsync(new SessionConfig { Tools = [AIFunctionFactory.Create(EncryptStringDenied, "encrypt_string")], - OnPermissionRequest = (request, invocation) => - { - return Task.FromResult(new PermissionRequestResult { Kind = "denied-interactively-by-user" }); - }, + OnPermissionRequest = async (request, invocation) => new() { Kind = "denied-interactively-by-user" }, }); await session.SendAsync(new MessageOptions diff --git a/nodejs/scripts/update-protocol-version.ts b/nodejs/scripts/update-protocol-version.ts index d0e3ecc66..46f6189e8 100644 --- a/nodejs/scripts/update-protocol-version.ts +++ b/nodejs/scripts/update-protocol-version.ts @@ -106,7 +106,7 @@ internal static class SdkProtocolVersion /// /// The SDK protocol version. /// - public const int Version = ${version}; + private const int Version = ${version}; /// /// Gets the SDK protocol version. diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index f9618b6dd..2fa8eb434 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -68,7 +68,13 @@ describe("CopilotClient", () => { onTestFinished(() => client.forceStop()); const session = await client.createSession({ onPermissionRequest: approveAll }); - const spy = vi.spyOn((client as any).connection!, "sendRequest"); + // Mock sendRequest to capture the call without hitting the runtime + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); await client.resumeSession(session.sessionId, { clientName: "my-app", onPermissionRequest: approveAll, @@ -78,6 +84,7 @@ describe("CopilotClient", () => { "session.resume", expect.objectContaining({ clientName: "my-app", sessionId: session.sessionId }) ); + spy.mockRestore(); }); it("sends session.model.switchTo RPC with correct params", async () => { @@ -325,7 +332,13 @@ describe("CopilotClient", () => { onTestFinished(() => client.forceStop()); const session = await client.createSession({ onPermissionRequest: approveAll }); - const spy = vi.spyOn((client as any).connection!, "sendRequest"); + // Mock sendRequest to capture the call without hitting the runtime + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); await client.resumeSession(session.sessionId, { onPermissionRequest: approveAll, tools: [ @@ -342,6 +355,7 @@ describe("CopilotClient", () => { expect(payload.tools).toEqual([ expect.objectContaining({ name: "grep", overridesBuiltInTool: true }), ]); + spy.mockRestore(); }); }); }); diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index f2e536257..a759c1135 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -393,6 +393,9 @@ function generateSessionEventsCode(schema: JSONSchema7): string { // AUTO-GENERATED FILE - DO NOT EDIT // Generated from: session-events.schema.json +// Generated code does not have XML doc comments; suppress CS1591 to avoid warnings. +#pragma warning disable CS1591 + using System.Text.Json; using System.Text.Json.Serialization; @@ -532,7 +535,8 @@ function emitRpcClass(className: string, schema: JSONSchema7, visibility: "publi let defaultVal = ""; if (isReq && !csharpType.endsWith("?")) { if (csharpType === "string") defaultVal = " = string.Empty;"; - else if (csharpType.startsWith("List<") || csharpType.startsWith("Dictionary<") || emittedRpcClasses.has(csharpType)) defaultVal = " = new();"; + else if (csharpType.startsWith("List<") || csharpType.startsWith("Dictionary<")) defaultVal = " = [];"; + else if (emittedRpcClasses.has(csharpType)) defaultVal = " = new();"; } lines.push(` public ${csharpType} ${csharpName} { get; set; }${defaultVal}`); if (i < props.length - 1) lines.push(""); @@ -738,6 +742,9 @@ function generateRpcCode(schema: ApiSchema): string { // AUTO-GENERATED FILE - DO NOT EDIT // Generated from: api.schema.json +// Generated code does not have XML doc comments; suppress CS1591 to avoid warnings. +#pragma warning disable CS1591 + using System.Text.Json; using System.Text.Json.Serialization; using StreamJsonRpc;