From 81c05d7c52636662e17776d36345120ee987bca3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:06:43 +0000 Subject: [PATCH 1/7] Initial plan From 7ec76c89273db4b7082fa5f20373acec1afbbd5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:12:17 +0000 Subject: [PATCH 2/7] Wrap StreamJsonRpc exceptions with CopilotRpcException Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com> --- dotnet/README.md | 4 +- dotnet/src/Client.cs | 117 ++++++++++++++++++++++++++++++------ dotnet/src/Session.cs | 54 +++++++++++++---- dotnet/src/Types.cs | 27 +++++++++ dotnet/test/SessionTests.cs | 4 +- 5 files changed, 169 insertions(+), 37 deletions(-) diff --git a/dotnet/README.md b/dotnet/README.md index e176da40..c12685eb 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -452,9 +452,9 @@ try var session = await client.CreateSessionAsync(); await session.SendAsync(new MessageOptions { Prompt = "Hello" }); } -catch (StreamJsonRpc.RemoteInvocationException ex) +catch (CopilotRpcException ex) { - Console.Error.WriteLine($"JSON-RPC Error: {ex.Message}"); + Console.Error.WriteLine($"RPC Error: {ex.Message}"); } catch (Exception ex) { diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index ef7982cb..60ff672c 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -347,8 +347,16 @@ public async Task CreateSessionAsync(SessionConfig? config = nul config?.DisabledSkills, config?.InfiniteSessions); - var response = await connection.Rpc.InvokeWithCancellationAsync( - "session.create", [request], cancellationToken); + CreateSessionResponse response; + try + { + response = await connection.Rpc.InvokeWithCancellationAsync( + "session.create", [request], cancellationToken); + } + catch (StreamJsonRpc.RemoteInvocationException ex) + { + throw new CopilotRpcException($"Failed to create session: {ex.Message}", ex); + } var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath); session.RegisterTools(config?.Tools ?? []); @@ -404,8 +412,16 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config?.SkillDirectories, config?.DisabledSkills); - var response = await connection.Rpc.InvokeWithCancellationAsync( - "session.resume", [request], cancellationToken); + ResumeSessionResponse response; + try + { + response = await connection.Rpc.InvokeWithCancellationAsync( + "session.resume", [request], cancellationToken); + } + catch (StreamJsonRpc.RemoteInvocationException ex) + { + throw new CopilotRpcException($"Failed to resume session: {ex.Message}", ex); + } var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath); session.RegisterTools(config?.Tools ?? []); @@ -461,8 +477,15 @@ public async Task PingAsync(string? message = null, CancellationTo { var connection = await EnsureConnectedAsync(cancellationToken); - return await connection.Rpc.InvokeWithCancellationAsync( - "ping", [new PingRequest { Message = message }], cancellationToken); + try + { + return await connection.Rpc.InvokeWithCancellationAsync( + "ping", [new PingRequest { Message = message }], cancellationToken); + } + catch (StreamJsonRpc.RemoteInvocationException ex) + { + throw new CopilotRpcException($"Failed to ping server: {ex.Message}", ex); + } } /// @@ -475,8 +498,15 @@ public async Task GetStatusAsync(CancellationToken cancellati { var connection = await EnsureConnectedAsync(cancellationToken); - return await connection.Rpc.InvokeWithCancellationAsync( - "status.get", [], cancellationToken); + try + { + return await connection.Rpc.InvokeWithCancellationAsync( + "status.get", [], cancellationToken); + } + catch (StreamJsonRpc.RemoteInvocationException ex) + { + throw new CopilotRpcException($"Failed to get status: {ex.Message}", ex); + } } /// @@ -489,8 +519,15 @@ public async Task GetAuthStatusAsync(CancellationToken ca { var connection = await EnsureConnectedAsync(cancellationToken); - return await connection.Rpc.InvokeWithCancellationAsync( - "auth.getStatus", [], cancellationToken); + try + { + return await connection.Rpc.InvokeWithCancellationAsync( + "auth.getStatus", [], cancellationToken); + } + catch (StreamJsonRpc.RemoteInvocationException ex) + { + throw new CopilotRpcException($"Failed to get auth status: {ex.Message}", ex); + } } /// @@ -503,8 +540,16 @@ public async Task> ListModelsAsync(CancellationToken cancellatio { var connection = await EnsureConnectedAsync(cancellationToken); - var response = await connection.Rpc.InvokeWithCancellationAsync( - "models.list", [], cancellationToken); + GetModelsResponse response; + try + { + response = await connection.Rpc.InvokeWithCancellationAsync( + "models.list", [], cancellationToken); + } + catch (StreamJsonRpc.RemoteInvocationException ex) + { + throw new CopilotRpcException($"Failed to list models: {ex.Message}", ex); + } return response.Models; } @@ -528,8 +573,16 @@ public async Task> ListModelsAsync(CancellationToken cancellatio { var connection = await EnsureConnectedAsync(cancellationToken); - var response = await connection.Rpc.InvokeWithCancellationAsync( - "session.getLastId", [], cancellationToken); + GetLastSessionIdResponse response; + try + { + response = await connection.Rpc.InvokeWithCancellationAsync( + "session.getLastId", [], cancellationToken); + } + catch (StreamJsonRpc.RemoteInvocationException ex) + { + throw new CopilotRpcException($"Failed to get last session ID: {ex.Message}", ex); + } return response.SessionId; } @@ -554,8 +607,16 @@ public async Task DeleteSessionAsync(string sessionId, CancellationToken cancell { var connection = await EnsureConnectedAsync(cancellationToken); - var response = await connection.Rpc.InvokeWithCancellationAsync( - "session.delete", [new DeleteSessionRequest(sessionId)], cancellationToken); + DeleteSessionResponse response; + try + { + response = await connection.Rpc.InvokeWithCancellationAsync( + "session.delete", [new DeleteSessionRequest(sessionId)], cancellationToken); + } + catch (StreamJsonRpc.RemoteInvocationException ex) + { + throw new CopilotRpcException($"Failed to delete session: {ex.Message}", ex); + } if (!response.Success) { @@ -584,8 +645,16 @@ public async Task> ListSessionsAsync(CancellationToken can { var connection = await EnsureConnectedAsync(cancellationToken); - var response = await connection.Rpc.InvokeWithCancellationAsync( - "session.list", [], cancellationToken); + ListSessionsResponse response; + try + { + response = await connection.Rpc.InvokeWithCancellationAsync( + "session.list", [], cancellationToken); + } + catch (StreamJsonRpc.RemoteInvocationException ex) + { + throw new CopilotRpcException($"Failed to list sessions: {ex.Message}", ex); + } return response.Sessions; } @@ -604,8 +673,16 @@ private Task EnsureConnectedAsync(CancellationToken cancellationToke private async Task VerifyProtocolVersionAsync(Connection connection, CancellationToken cancellationToken) { var expectedVersion = SdkProtocolVersion.GetVersion(); - var pingResponse = await connection.Rpc.InvokeWithCancellationAsync( - "ping", [new PingRequest()], cancellationToken); + PingResponse pingResponse; + try + { + pingResponse = await connection.Rpc.InvokeWithCancellationAsync( + "ping", [new PingRequest()], cancellationToken); + } + catch (StreamJsonRpc.RemoteInvocationException ex) + { + throw new CopilotRpcException($"Failed to verify protocol version: {ex.Message}", ex); + } if (!pingResponse.ProtocolVersion.HasValue) { diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index f1e47df8..6b9fde78 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -118,10 +118,17 @@ public async Task SendAsync(MessageOptions options, CancellationToken ca Mode = options.Mode }; - var response = await _rpc.InvokeWithCancellationAsync( - "session.send", [request], cancellationToken); + try + { + var response = await _rpc.InvokeWithCancellationAsync( + "session.send", [request], cancellationToken); - return response.MessageId; + return response.MessageId; + } + catch (StreamJsonRpc.RemoteInvocationException ex) + { + throw new CopilotRpcException($"Failed to send message: {ex.Message}", ex); + } } /// @@ -351,13 +358,20 @@ internal async Task HandlePermissionRequestAsync(JsonEl /// public async Task> GetMessagesAsync(CancellationToken cancellationToken = default) { - var response = await _rpc.InvokeWithCancellationAsync( - "session.getMessages", [new GetMessagesRequest { SessionId = SessionId }], cancellationToken); + try + { + var response = await _rpc.InvokeWithCancellationAsync( + "session.getMessages", [new GetMessagesRequest { SessionId = SessionId }], cancellationToken); - return response.Events - .Select(e => SessionEvent.FromJson(e.ToJsonString())) - .OfType() - .ToList(); + return response.Events + .Select(e => SessionEvent.FromJson(e.ToJsonString())) + .OfType() + .ToList(); + } + catch (StreamJsonRpc.RemoteInvocationException ex) + { + throw new CopilotRpcException($"Failed to get messages: {ex.Message}", ex); + } } /// @@ -385,8 +399,15 @@ public async Task> GetMessagesAsync(CancellationToke /// public async Task AbortAsync(CancellationToken cancellationToken = default) { - await _rpc.InvokeWithCancellationAsync( - "session.abort", [new SessionAbortRequest { SessionId = SessionId }], cancellationToken); + try + { + await _rpc.InvokeWithCancellationAsync( + "session.abort", [new SessionAbortRequest { SessionId = SessionId }], cancellationToken); + } + catch (StreamJsonRpc.RemoteInvocationException ex) + { + throw new CopilotRpcException($"Failed to abort session: {ex.Message}", ex); + } } /// @@ -416,8 +437,15 @@ await _rpc.InvokeWithCancellationAsync( /// public async ValueTask DisposeAsync() { - await _rpc.InvokeWithCancellationAsync( - "session.destroy", [new SessionDestroyRequest() { SessionId = SessionId }]); + try + { + await _rpc.InvokeWithCancellationAsync( + "session.destroy", [new SessionDestroyRequest() { SessionId = SessionId }]); + } + catch (StreamJsonRpc.RemoteInvocationException ex) + { + throw new CopilotRpcException($"Failed to destroy session: {ex.Message}", ex); + } _eventHandlers.Clear(); _toolHandlers.Clear(); diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 24b4fc2e..22e1a72d 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -9,6 +9,33 @@ namespace GitHub.Copilot.SDK; +/// +/// Exception thrown when a JSON-RPC communication error occurs with the Copilot CLI. +/// +/// +/// This exception wraps underlying communication errors to provide a stable API surface +/// that doesn't expose implementation details of the JSON-RPC protocol. +/// +public class CopilotRpcException : IOException +{ + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + public CopilotRpcException(string message) : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + public CopilotRpcException(string message, Exception innerException) : base(message, innerException) + { + } +} + [JsonConverter(typeof(JsonStringEnumConverter))] public enum ConnectionState { diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index 845e604a..11a030a8 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -26,7 +26,7 @@ public async Task ShouldCreateAndDestroySessions() await session.DisposeAsync(); - var ex = await Assert.ThrowsAsync(() => session.GetMessagesAsync()); + var ex = await Assert.ThrowsAsync(() => session.GetMessagesAsync()); Assert.Contains("not found", ex.Message, StringComparison.OrdinalIgnoreCase); } @@ -192,7 +192,7 @@ public async Task Should_Resume_A_Session_Using_A_New_Client() [Fact] public async Task Should_Throw_Error_When_Resuming_Non_Existent_Session() { - await Assert.ThrowsAsync(() => + await Assert.ThrowsAsync(() => Client.ResumeSessionAsync("non-existent-session-id")); } From 8598f713d35a070e728ae76516da93397a1ed82c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:15:13 +0000 Subject: [PATCH 3/7] Add explicit using System.IO for IOException Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com> --- dotnet/src/Types.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 22e1a72d..ce0f2d39 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using System.IO; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.AI; From 81d8decd10db88a66b828475ba856bb78e57c050 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:34:18 +0000 Subject: [PATCH 4/7] Only wrap RPC invocation calls in try-catch blocks Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com> --- dotnet/src/Client.cs | 15 ++++++++++++--- dotnet/src/Session.cs | 20 +++++++++++--------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 60ff672c..fefb6b38 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -477,15 +477,18 @@ public async Task PingAsync(string? message = null, CancellationTo { var connection = await EnsureConnectedAsync(cancellationToken); + PingResponse response; try { - return await connection.Rpc.InvokeWithCancellationAsync( + response = await connection.Rpc.InvokeWithCancellationAsync( "ping", [new PingRequest { Message = message }], cancellationToken); } catch (StreamJsonRpc.RemoteInvocationException ex) { throw new CopilotRpcException($"Failed to ping server: {ex.Message}", ex); } + + return response; } /// @@ -498,15 +501,18 @@ public async Task GetStatusAsync(CancellationToken cancellati { var connection = await EnsureConnectedAsync(cancellationToken); + GetStatusResponse response; try { - return await connection.Rpc.InvokeWithCancellationAsync( + response = await connection.Rpc.InvokeWithCancellationAsync( "status.get", [], cancellationToken); } catch (StreamJsonRpc.RemoteInvocationException ex) { throw new CopilotRpcException($"Failed to get status: {ex.Message}", ex); } + + return response; } /// @@ -519,15 +525,18 @@ public async Task GetAuthStatusAsync(CancellationToken ca { var connection = await EnsureConnectedAsync(cancellationToken); + GetAuthStatusResponse response; try { - return await connection.Rpc.InvokeWithCancellationAsync( + response = await connection.Rpc.InvokeWithCancellationAsync( "auth.getStatus", [], cancellationToken); } catch (StreamJsonRpc.RemoteInvocationException ex) { throw new CopilotRpcException($"Failed to get auth status: {ex.Message}", ex); } + + return response; } /// diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 6b9fde78..9c5831ab 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -118,17 +118,18 @@ public async Task SendAsync(MessageOptions options, CancellationToken ca Mode = options.Mode }; + SendMessageResponse response; try { - var response = await _rpc.InvokeWithCancellationAsync( + response = await _rpc.InvokeWithCancellationAsync( "session.send", [request], cancellationToken); - - return response.MessageId; } catch (StreamJsonRpc.RemoteInvocationException ex) { throw new CopilotRpcException($"Failed to send message: {ex.Message}", ex); } + + return response.MessageId; } /// @@ -358,20 +359,21 @@ internal async Task HandlePermissionRequestAsync(JsonEl /// public async Task> GetMessagesAsync(CancellationToken cancellationToken = default) { + GetMessagesResponse response; try { - var response = await _rpc.InvokeWithCancellationAsync( + response = await _rpc.InvokeWithCancellationAsync( "session.getMessages", [new GetMessagesRequest { SessionId = SessionId }], cancellationToken); - - return response.Events - .Select(e => SessionEvent.FromJson(e.ToJsonString())) - .OfType() - .ToList(); } catch (StreamJsonRpc.RemoteInvocationException ex) { throw new CopilotRpcException($"Failed to get messages: {ex.Message}", ex); } + + return response.Events + .Select(e => SessionEvent.FromJson(e.ToJsonString())) + .OfType() + .ToList(); } /// From f46b3d5afc50c7788d492e0a6c0895d0fe666d2d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:38:49 +0000 Subject: [PATCH 5/7] Remove implementation details from CopilotRpcException docs Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com> --- dotnet/src/Types.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index ce0f2d39..1e1bcbc9 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -11,12 +11,8 @@ namespace GitHub.Copilot.SDK; /// -/// Exception thrown when a JSON-RPC communication error occurs with the Copilot CLI. +/// Exception thrown when a communication error occurs with the Copilot CLI. /// -/// -/// This exception wraps underlying communication errors to provide a stable API surface -/// that doesn't expose implementation details of the JSON-RPC protocol. -/// public class CopilotRpcException : IOException { /// From 63b974ad5a09f79d324469d98ceee5285aa6eddc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:07:10 +0000 Subject: [PATCH 6/7] Replace CopilotRpcException with IOException and add RPC wrapper methods Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com> --- dotnet/README.md | 4 +- dotnet/src/Client.cs | 130 ++++++++---------------------------- dotnet/src/Session.cs | 58 ++++++---------- dotnet/src/Types.cs | 23 ------- dotnet/test/SessionTests.cs | 4 +- 5 files changed, 52 insertions(+), 167 deletions(-) diff --git a/dotnet/README.md b/dotnet/README.md index c12685eb..c63bb120 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -452,9 +452,9 @@ try var session = await client.CreateSessionAsync(); await session.SendAsync(new MessageOptions { Prompt = "Hello" }); } -catch (CopilotRpcException ex) +catch (IOException ex) { - Console.Error.WriteLine($"RPC Error: {ex.Message}"); + Console.Error.WriteLine($"Communication Error: {ex.Message}"); } catch (Exception ex) { diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index fefb6b38..c610c567 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -347,16 +347,8 @@ public async Task CreateSessionAsync(SessionConfig? config = nul config?.DisabledSkills, config?.InfiniteSessions); - CreateSessionResponse response; - try - { - response = await connection.Rpc.InvokeWithCancellationAsync( - "session.create", [request], cancellationToken); - } - catch (StreamJsonRpc.RemoteInvocationException ex) - { - throw new CopilotRpcException($"Failed to create session: {ex.Message}", ex); - } + var response = await InvokeRpcAsync( + connection.Rpc, "session.create", [request], cancellationToken); var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath); session.RegisterTools(config?.Tools ?? []); @@ -412,16 +404,8 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config?.SkillDirectories, config?.DisabledSkills); - ResumeSessionResponse response; - try - { - response = await connection.Rpc.InvokeWithCancellationAsync( - "session.resume", [request], cancellationToken); - } - catch (StreamJsonRpc.RemoteInvocationException ex) - { - throw new CopilotRpcException($"Failed to resume session: {ex.Message}", ex); - } + var response = await InvokeRpcAsync( + connection.Rpc, "session.resume", [request], cancellationToken); var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath); session.RegisterTools(config?.Tools ?? []); @@ -477,18 +461,8 @@ public async Task PingAsync(string? message = null, CancellationTo { var connection = await EnsureConnectedAsync(cancellationToken); - PingResponse response; - try - { - response = await connection.Rpc.InvokeWithCancellationAsync( - "ping", [new PingRequest { Message = message }], cancellationToken); - } - catch (StreamJsonRpc.RemoteInvocationException ex) - { - throw new CopilotRpcException($"Failed to ping server: {ex.Message}", ex); - } - - return response; + return await InvokeRpcAsync( + connection.Rpc, "ping", [new PingRequest { Message = message }], cancellationToken); } /// @@ -501,18 +475,8 @@ public async Task GetStatusAsync(CancellationToken cancellati { var connection = await EnsureConnectedAsync(cancellationToken); - GetStatusResponse response; - try - { - response = await connection.Rpc.InvokeWithCancellationAsync( - "status.get", [], cancellationToken); - } - catch (StreamJsonRpc.RemoteInvocationException ex) - { - throw new CopilotRpcException($"Failed to get status: {ex.Message}", ex); - } - - return response; + return await InvokeRpcAsync( + connection.Rpc, "status.get", [], cancellationToken); } /// @@ -525,18 +489,8 @@ public async Task GetAuthStatusAsync(CancellationToken ca { var connection = await EnsureConnectedAsync(cancellationToken); - GetAuthStatusResponse response; - try - { - response = await connection.Rpc.InvokeWithCancellationAsync( - "auth.getStatus", [], cancellationToken); - } - catch (StreamJsonRpc.RemoteInvocationException ex) - { - throw new CopilotRpcException($"Failed to get auth status: {ex.Message}", ex); - } - - return response; + return await InvokeRpcAsync( + connection.Rpc, "auth.getStatus", [], cancellationToken); } /// @@ -549,16 +503,8 @@ public async Task> ListModelsAsync(CancellationToken cancellatio { var connection = await EnsureConnectedAsync(cancellationToken); - GetModelsResponse response; - try - { - response = await connection.Rpc.InvokeWithCancellationAsync( - "models.list", [], cancellationToken); - } - catch (StreamJsonRpc.RemoteInvocationException ex) - { - throw new CopilotRpcException($"Failed to list models: {ex.Message}", ex); - } + var response = await InvokeRpcAsync( + connection.Rpc, "models.list", [], cancellationToken); return response.Models; } @@ -582,16 +528,8 @@ public async Task> ListModelsAsync(CancellationToken cancellatio { var connection = await EnsureConnectedAsync(cancellationToken); - GetLastSessionIdResponse response; - try - { - response = await connection.Rpc.InvokeWithCancellationAsync( - "session.getLastId", [], cancellationToken); - } - catch (StreamJsonRpc.RemoteInvocationException ex) - { - throw new CopilotRpcException($"Failed to get last session ID: {ex.Message}", ex); - } + var response = await InvokeRpcAsync( + connection.Rpc, "session.getLastId", [], cancellationToken); return response.SessionId; } @@ -616,16 +554,8 @@ public async Task DeleteSessionAsync(string sessionId, CancellationToken cancell { var connection = await EnsureConnectedAsync(cancellationToken); - DeleteSessionResponse response; - try - { - response = await connection.Rpc.InvokeWithCancellationAsync( - "session.delete", [new DeleteSessionRequest(sessionId)], cancellationToken); - } - catch (StreamJsonRpc.RemoteInvocationException ex) - { - throw new CopilotRpcException($"Failed to delete session: {ex.Message}", ex); - } + var response = await InvokeRpcAsync( + connection.Rpc, "session.delete", [new DeleteSessionRequest(sessionId)], cancellationToken); if (!response.Success) { @@ -654,18 +584,22 @@ public async Task> ListSessionsAsync(CancellationToken can { var connection = await EnsureConnectedAsync(cancellationToken); - ListSessionsResponse response; + var response = await InvokeRpcAsync( + connection.Rpc, "session.list", [], cancellationToken); + + return response.Sessions; + } + + private static async Task InvokeRpcAsync(JsonRpc rpc, string method, object?[]? args, CancellationToken cancellationToken) + { try { - response = await connection.Rpc.InvokeWithCancellationAsync( - "session.list", [], cancellationToken); + return await rpc.InvokeWithCancellationAsync(method, args, cancellationToken); } catch (StreamJsonRpc.RemoteInvocationException ex) { - throw new CopilotRpcException($"Failed to list sessions: {ex.Message}", ex); + throw new IOException($"Communication error with Copilot CLI: {ex.Message}", ex); } - - return response.Sessions; } private Task EnsureConnectedAsync(CancellationToken cancellationToken) @@ -682,16 +616,8 @@ private Task EnsureConnectedAsync(CancellationToken cancellationToke private async Task VerifyProtocolVersionAsync(Connection connection, CancellationToken cancellationToken) { var expectedVersion = SdkProtocolVersion.GetVersion(); - PingResponse pingResponse; - try - { - pingResponse = await connection.Rpc.InvokeWithCancellationAsync( - "ping", [new PingRequest()], cancellationToken); - } - catch (StreamJsonRpc.RemoteInvocationException ex) - { - throw new CopilotRpcException($"Failed to verify protocol version: {ex.Message}", ex); - } + var pingResponse = await InvokeRpcAsync( + connection.Rpc, "ping", [new PingRequest()], cancellationToken); if (!pingResponse.ProtocolVersion.HasValue) { diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 9c5831ab..d6f2192e 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -80,6 +80,18 @@ internal CopilotSession(string sessionId, JsonRpc rpc, string? workspacePath = n WorkspacePath = workspacePath; } + private async Task InvokeRpcAsync(string method, object?[]? args, CancellationToken cancellationToken) + { + try + { + return await _rpc.InvokeWithCancellationAsync(method, args, cancellationToken); + } + catch (StreamJsonRpc.RemoteInvocationException ex) + { + throw new IOException($"Communication error with Copilot CLI: {ex.Message}", ex); + } + } + /// /// Sends a message to the Copilot session and waits for the response. /// @@ -118,16 +130,8 @@ public async Task SendAsync(MessageOptions options, CancellationToken ca Mode = options.Mode }; - SendMessageResponse response; - try - { - response = await _rpc.InvokeWithCancellationAsync( - "session.send", [request], cancellationToken); - } - catch (StreamJsonRpc.RemoteInvocationException ex) - { - throw new CopilotRpcException($"Failed to send message: {ex.Message}", ex); - } + var response = await InvokeRpcAsync( + "session.send", [request], cancellationToken); return response.MessageId; } @@ -359,16 +363,8 @@ internal async Task HandlePermissionRequestAsync(JsonEl /// public async Task> GetMessagesAsync(CancellationToken cancellationToken = default) { - GetMessagesResponse response; - try - { - response = await _rpc.InvokeWithCancellationAsync( - "session.getMessages", [new GetMessagesRequest { SessionId = SessionId }], cancellationToken); - } - catch (StreamJsonRpc.RemoteInvocationException ex) - { - throw new CopilotRpcException($"Failed to get messages: {ex.Message}", ex); - } + var response = await InvokeRpcAsync( + "session.getMessages", [new GetMessagesRequest { SessionId = SessionId }], cancellationToken); return response.Events .Select(e => SessionEvent.FromJson(e.ToJsonString())) @@ -401,15 +397,8 @@ public async Task> GetMessagesAsync(CancellationToke /// public async Task AbortAsync(CancellationToken cancellationToken = default) { - try - { - await _rpc.InvokeWithCancellationAsync( - "session.abort", [new SessionAbortRequest { SessionId = SessionId }], cancellationToken); - } - catch (StreamJsonRpc.RemoteInvocationException ex) - { - throw new CopilotRpcException($"Failed to abort session: {ex.Message}", ex); - } + await InvokeRpcAsync( + "session.abort", [new SessionAbortRequest { SessionId = SessionId }], cancellationToken); } /// @@ -439,15 +428,8 @@ await _rpc.InvokeWithCancellationAsync( /// public async ValueTask DisposeAsync() { - try - { - await _rpc.InvokeWithCancellationAsync( - "session.destroy", [new SessionDestroyRequest() { SessionId = SessionId }]); - } - catch (StreamJsonRpc.RemoteInvocationException ex) - { - throw new CopilotRpcException($"Failed to destroy session: {ex.Message}", ex); - } + await InvokeRpcAsync( + "session.destroy", [new SessionDestroyRequest() { SessionId = SessionId }], CancellationToken.None); _eventHandlers.Clear(); _toolHandlers.Clear(); diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 1e1bcbc9..1195f2e8 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -10,29 +10,6 @@ namespace GitHub.Copilot.SDK; -/// -/// Exception thrown when a communication error occurs with the Copilot CLI. -/// -public class CopilotRpcException : IOException -{ - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - public CopilotRpcException(string message) : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception. - public CopilotRpcException(string message, Exception innerException) : base(message, innerException) - { - } -} - [JsonConverter(typeof(JsonStringEnumConverter))] public enum ConnectionState { diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index 11a030a8..13b23522 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -26,7 +26,7 @@ public async Task ShouldCreateAndDestroySessions() await session.DisposeAsync(); - var ex = await Assert.ThrowsAsync(() => session.GetMessagesAsync()); + var ex = await Assert.ThrowsAsync(() => session.GetMessagesAsync()); Assert.Contains("not found", ex.Message, StringComparison.OrdinalIgnoreCase); } @@ -192,7 +192,7 @@ public async Task Should_Resume_A_Session_Using_A_New_Client() [Fact] public async Task Should_Throw_Error_When_Resuming_Non_Existent_Session() { - await Assert.ThrowsAsync(() => + await Assert.ThrowsAsync(() => Client.ResumeSessionAsync("non-existent-session-id")); } From bad23c1e09c2589e0119bc499fac33e74985f939 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:47:14 +0000 Subject: [PATCH 7/7] Catch RemoteRpcException, refactor Session.InvokeRpcAsync, remove unused using Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com> --- dotnet/src/Client.cs | 4 ++-- dotnet/src/Session.cs | 13 ++----------- dotnet/src/Types.cs | 1 - 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index c610c567..88946eef 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -590,13 +590,13 @@ public async Task> ListSessionsAsync(CancellationToken can return response.Sessions; } - private static async Task InvokeRpcAsync(JsonRpc rpc, string method, object?[]? args, CancellationToken cancellationToken) + internal static async Task InvokeRpcAsync(JsonRpc rpc, string method, object?[]? args, CancellationToken cancellationToken) { try { return await rpc.InvokeWithCancellationAsync(method, args, cancellationToken); } - catch (StreamJsonRpc.RemoteInvocationException ex) + catch (StreamJsonRpc.RemoteRpcException ex) { throw new IOException($"Communication error with Copilot CLI: {ex.Message}", ex); } diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index d6f2192e..7f1cc4e4 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -80,17 +80,8 @@ internal CopilotSession(string sessionId, JsonRpc rpc, string? workspacePath = n WorkspacePath = workspacePath; } - private async Task InvokeRpcAsync(string method, object?[]? args, CancellationToken cancellationToken) - { - try - { - return await _rpc.InvokeWithCancellationAsync(method, args, cancellationToken); - } - catch (StreamJsonRpc.RemoteInvocationException ex) - { - throw new IOException($"Communication error with Copilot CLI: {ex.Message}", ex); - } - } + private Task InvokeRpcAsync(string method, object?[]? args, CancellationToken cancellationToken) => + CopilotClient.InvokeRpcAsync(_rpc, method, args, cancellationToken); /// /// Sends a message to the Copilot session and waits for the response. diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 1195f2e8..24b4fc2e 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ -using System.IO; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.AI;