@@ -54,6 +54,11 @@ namespace GitHub.Copilot.SDK;
5454/// </example>
5555public sealed partial class CopilotClient : IDisposable , IAsyncDisposable
5656{
57+ /// <summary>
58+ /// Minimum protocol version this SDK can communicate with.
59+ /// </summary>
60+ private const int MinProtocolVersion = 2 ;
61+
5762 private readonly ConcurrentDictionary < string , CopilotSession > _sessions = new ( ) ;
5863 private readonly CopilotClientOptions _options ;
5964 private readonly ILogger _logger ;
@@ -62,6 +67,7 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
6267 private readonly int ? _optionsPort ;
6368 private readonly string ? _optionsHost ;
6469 private int ? _actualPort ;
70+ private int ? _negotiatedProtocolVersion ;
6571 private List < ModelInfo > ? _modelsCache ;
6672 private readonly SemaphoreSlim _modelsCacheLock = new ( 1 , 1 ) ;
6773 private readonly List < Action < SessionLifecycleEvent > > _lifecycleHandlers = [ ] ;
@@ -923,27 +929,30 @@ private Task<Connection> EnsureConnectedAsync(CancellationToken cancellationToke
923929 return ( Task < Connection > ) StartAsync ( cancellationToken ) ;
924930 }
925931
926- private static async Task VerifyProtocolVersionAsync ( Connection connection , CancellationToken cancellationToken )
932+ private async Task VerifyProtocolVersionAsync ( Connection connection , CancellationToken cancellationToken )
927933 {
928- var expectedVersion = SdkProtocolVersion . GetVersion ( ) ;
934+ var maxVersion = SdkProtocolVersion . GetVersion ( ) ;
929935 var pingResponse = await InvokeRpcAsync < PingResponse > (
930936 connection . Rpc , "ping" , [ new PingRequest ( ) ] , connection . StderrBuffer , cancellationToken ) ;
931937
932938 if ( ! pingResponse . ProtocolVersion . HasValue )
933939 {
934940 throw new InvalidOperationException (
935- $ "SDK protocol version mismatch: SDK expects version { expectedVersion } , " +
941+ $ "SDK protocol version mismatch: SDK expects version { MinProtocolVersion } - { maxVersion } , " +
936942 $ "but server does not report a protocol version. " +
937943 $ "Please update your server to ensure compatibility.") ;
938944 }
939945
940- if ( pingResponse . ProtocolVersion . Value != expectedVersion )
946+ var serverVersion = pingResponse . ProtocolVersion . Value ;
947+ if ( serverVersion < MinProtocolVersion || serverVersion > maxVersion )
941948 {
942949 throw new InvalidOperationException (
943- $ "SDK protocol version mismatch: SDK expects version { expectedVersion } , " +
944- $ "but server reports version { pingResponse . ProtocolVersion . Value } . " +
950+ $ "SDK protocol version mismatch: SDK supports versions { MinProtocolVersion } - { maxVersion } , " +
951+ $ "but server reports version { serverVersion } . " +
945952 $ "Please update your SDK or server to ensure compatibility.") ;
946953 }
954+
955+ _negotiatedProtocolVersion = serverVersion ;
947956 }
948957
949958 private static async Task < ( Process Process , int ? DetectedLocalhostTcpPort , StringBuilder StderrBuffer ) > StartCliServerAsync ( CopilotClientOptions options , ILogger logger , CancellationToken cancellationToken )
@@ -1137,6 +1146,12 @@ private async Task<Connection> ConnectToServerAsync(Process? cliProcess, string?
11371146 var handler = new RpcHandler ( this ) ;
11381147 rpc . AddLocalRpcMethod ( "session.event" , handler . OnSessionEvent ) ;
11391148 rpc . AddLocalRpcMethod ( "session.lifecycle" , handler . OnSessionLifecycle ) ;
1149+ // Protocol v3 servers send tool calls / permission requests as broadcast events.
1150+ // Protocol v2 servers use the older tool.call / permission.request RPC model.
1151+ // We always register v2 adapters because handlers are set up before version
1152+ // negotiation; a v3 server will simply never send these requests.
1153+ rpc . AddLocalRpcMethod ( "tool.call" , handler . OnToolCallV2 ) ;
1154+ rpc . AddLocalRpcMethod ( "permission.request" , handler . OnPermissionRequestV2 ) ;
11401155 rpc . AddLocalRpcMethod ( "userInput.request" , handler . OnUserInputRequest ) ;
11411156 rpc . AddLocalRpcMethod ( "hooks.invoke" , handler . OnHooksInvoke ) ;
11421157 rpc . StartListening ( ) ;
@@ -1257,6 +1272,102 @@ public async Task<HooksInvokeResponse> OnHooksInvoke(string sessionId, string ho
12571272 var output = await session . HandleHooksInvokeAsync ( hookType , input ) ;
12581273 return new HooksInvokeResponse ( output ) ;
12591274 }
1275+
1276+ // Protocol v2 backward-compatibility adapters
1277+
1278+ public async Task < ToolCallResponseV2 > OnToolCallV2 ( string sessionId ,
1279+ string toolCallId ,
1280+ string toolName ,
1281+ object ? arguments )
1282+ {
1283+ var session = client . GetSession ( sessionId ) ?? throw new ArgumentException ( $ "Unknown session { sessionId } ") ;
1284+ if ( session . GetTool ( toolName ) is not { } tool )
1285+ {
1286+ return new ToolCallResponseV2 ( new ToolResultObject
1287+ {
1288+ TextResultForLlm = $ "Tool '{ toolName } ' is not supported.",
1289+ ResultType = "failure" ,
1290+ Error = $ "tool '{ toolName } ' not supported"
1291+ } ) ;
1292+ }
1293+
1294+ try
1295+ {
1296+ var invocation = new ToolInvocation
1297+ {
1298+ SessionId = sessionId ,
1299+ ToolCallId = toolCallId ,
1300+ ToolName = toolName ,
1301+ Arguments = arguments
1302+ } ;
1303+
1304+ var aiFunctionArgs = new AIFunctionArguments
1305+ {
1306+ Context = new Dictionary < object , object ? >
1307+ {
1308+ [ typeof ( ToolInvocation ) ] = invocation
1309+ }
1310+ } ;
1311+
1312+ if ( arguments is not null )
1313+ {
1314+ if ( arguments is not JsonElement incomingJsonArgs )
1315+ {
1316+ throw new InvalidOperationException ( $ "Incoming arguments must be a { nameof ( JsonElement ) } ; received { arguments . GetType ( ) . Name } ") ;
1317+ }
1318+
1319+ foreach ( var prop in incomingJsonArgs . EnumerateObject ( ) )
1320+ {
1321+ aiFunctionArgs [ prop . Name ] = prop . Value ;
1322+ }
1323+ }
1324+
1325+ var result = await tool . InvokeAsync ( aiFunctionArgs ) ;
1326+
1327+ var toolResultObject = result is ToolResultAIContent trac ? trac . Result : new ToolResultObject
1328+ {
1329+ ResultType = "success" ,
1330+ TextResultForLlm = result is JsonElement { ValueKind : JsonValueKind . String } je
1331+ ? je . GetString ( ) !
1332+ : JsonSerializer . Serialize ( result , tool . JsonSerializerOptions . GetTypeInfo ( typeof ( object ) ) ) ,
1333+ } ;
1334+ return new ToolCallResponseV2 ( toolResultObject ) ;
1335+ }
1336+ catch ( Exception ex )
1337+ {
1338+ return new ToolCallResponseV2 ( new ToolResultObject
1339+ {
1340+ TextResultForLlm = "Invoking this tool produced an error. Detailed information is not available." ,
1341+ ResultType = "failure" ,
1342+ Error = ex . Message
1343+ } ) ;
1344+ }
1345+ }
1346+
1347+ public async Task < PermissionRequestResponseV2 > OnPermissionRequestV2 ( string sessionId , JsonElement permissionRequest )
1348+ {
1349+ var session = client . GetSession ( sessionId ) ;
1350+ if ( session == null )
1351+ {
1352+ return new PermissionRequestResponseV2 ( new PermissionRequestResult
1353+ {
1354+ Kind = PermissionRequestResultKind . DeniedCouldNotRequestFromUser
1355+ } ) ;
1356+ }
1357+
1358+ try
1359+ {
1360+ var result = await session . HandlePermissionRequestAsync ( permissionRequest ) ;
1361+ return new PermissionRequestResponseV2 ( result ) ;
1362+ }
1363+ catch
1364+ {
1365+ return new PermissionRequestResponseV2 ( new PermissionRequestResult
1366+ {
1367+ Kind = PermissionRequestResultKind . DeniedCouldNotRequestFromUser
1368+ } ) ;
1369+ }
1370+ }
12601371 }
12611372
12621373 private class Connection (
@@ -1376,6 +1487,13 @@ internal record UserInputRequestResponse(
13761487 internal record HooksInvokeResponse (
13771488 object ? Output ) ;
13781489
1490+ // Protocol v2 backward-compatibility response types
1491+ internal record ToolCallResponseV2 (
1492+ ToolResultObject ? Result ) ;
1493+
1494+ internal record PermissionRequestResponseV2 (
1495+ PermissionRequestResult Result ) ;
1496+
13791497 /// <summary>Trace source that forwards all logs to the ILogger.</summary>
13801498 internal sealed class LoggerTraceSource : TraceSource
13811499 {
@@ -1469,11 +1587,13 @@ private static LogLevel MapLevel(TraceEventType eventType)
14691587 [ JsonSerializable ( typeof ( ListSessionsRequest ) ) ]
14701588 [ JsonSerializable ( typeof ( ListSessionsResponse ) ) ]
14711589 [ JsonSerializable ( typeof ( PermissionRequestResult ) ) ]
1590+ [ JsonSerializable ( typeof ( PermissionRequestResponseV2 ) ) ]
14721591 [ JsonSerializable ( typeof ( ProviderConfig ) ) ]
14731592 [ JsonSerializable ( typeof ( ResumeSessionRequest ) ) ]
14741593 [ JsonSerializable ( typeof ( ResumeSessionResponse ) ) ]
14751594 [ JsonSerializable ( typeof ( SessionMetadata ) ) ]
14761595 [ JsonSerializable ( typeof ( SystemMessageConfig ) ) ]
1596+ [ JsonSerializable ( typeof ( ToolCallResponseV2 ) ) ]
14771597 [ JsonSerializable ( typeof ( ToolDefinition ) ) ]
14781598 [ JsonSerializable ( typeof ( ToolResultAIContent ) ) ]
14791599 [ JsonSerializable ( typeof ( ToolResultObject ) ) ]
0 commit comments