@@ -67,6 +67,8 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
6767 private readonly Dictionary < string , List < Action < SessionLifecycleEvent > > > _typedLifecycleHandlers = [ ] ;
6868 private readonly object _lifecycleHandlersLock = new ( ) ;
6969 private ServerRpc ? _rpc ;
70+ private JsonRpc ? _jsonRpc ;
71+ private int _negotiatedProtocolVersion ;
7072
7173 /// <summary>
7274 /// Gets the typed RPC client for server-scoped methods (no session required).
@@ -318,6 +320,8 @@ private async Task CleanupConnectionAsync(List<Exception>? errors)
318320
319321 // Clear RPC and models cache
320322 _rpc = null ;
323+ _jsonRpc = null ;
324+ _negotiatedProtocolVersion = 0 ;
321325 _modelsCache = null ;
322326
323327 if ( ctx . NetworkStream is not null )
@@ -915,27 +919,31 @@ private Task<Connection> EnsureConnectedAsync(CancellationToken cancellationToke
915919 return ( Task < Connection > ) StartAsync ( cancellationToken ) ;
916920 }
917921
918- private static async Task VerifyProtocolVersionAsync ( Connection connection , CancellationToken cancellationToken )
922+ private async Task VerifyProtocolVersionAsync ( Connection connection , CancellationToken cancellationToken )
919923 {
920- var expectedVersion = SdkProtocolVersion . GetVersion ( ) ;
924+ var maxVersion = SdkProtocolVersion . GetVersion ( ) ;
925+ var minVersion = SdkProtocolVersion . GetMinVersion ( ) ;
921926 var pingResponse = await InvokeRpcAsync < PingResponse > (
922927 connection . Rpc , "ping" , [ new PingRequest ( ) ] , connection . StderrBuffer , cancellationToken ) ;
923928
924929 if ( ! pingResponse . ProtocolVersion . HasValue )
925930 {
926931 throw new InvalidOperationException (
927- $ "SDK protocol version mismatch: SDK expects version { expectedVersion } , " +
932+ $ "SDK protocol version mismatch: SDK supports versions { minVersion } - { maxVersion } , " +
928933 $ "but server does not report a protocol version. " +
929934 $ "Please update your server to ensure compatibility.") ;
930935 }
931936
932- if ( pingResponse . ProtocolVersion . Value != expectedVersion )
937+ var serverVersion = pingResponse . ProtocolVersion . Value ;
938+ if ( serverVersion < minVersion || serverVersion > maxVersion )
933939 {
934940 throw new InvalidOperationException (
935- $ "SDK protocol version mismatch: SDK expects version { expectedVersion } , " +
936- $ "but server reports version { pingResponse . ProtocolVersion . Value } . " +
941+ $ "SDK protocol version mismatch: SDK supports versions { minVersion } - { maxVersion } , " +
942+ $ "but server reports version { serverVersion } . " +
937943 $ "Please update your SDK or server to ensure compatibility.") ;
938944 }
945+
946+ _negotiatedProtocolVersion = serverVersion ;
939947 }
940948
941949 private static async Task < ( Process Process , int ? DetectedLocalhostTcpPort , StringBuilder StderrBuffer ) > StartCliServerAsync ( CopilotClientOptions options , ILogger logger , CancellationToken cancellationToken )
@@ -1135,6 +1143,7 @@ private async Task<Connection> ConnectToServerAsync(Process? cliProcess, string?
11351143 rpc . AddLocalRpcMethod ( "hooks.invoke" , handler . OnHooksInvoke ) ;
11361144 rpc . StartListening ( ) ;
11371145
1146+ _jsonRpc = rpc ;
11381147 _rpc = new ServerRpc ( rpc ) ;
11391148
11401149 return new Connection ( rpc , cliProcess , tcpClient , networkStream , stderrBuffer ) ;
@@ -1173,6 +1182,145 @@ private static JsonSerializerOptions CreateSerializerOptions()
11731182 return _sessions . TryGetValue ( sessionId , out var session ) ? session : null ;
11741183 }
11751184
1185+ private async Task HandleExternalToolRequestedAsync ( string sessionId , JsonElement eventJson )
1186+ {
1187+ string ? requestId = null ;
1188+ try
1189+ {
1190+ if ( ! eventJson . TryGetProperty ( "data" , out var data ) ) return ;
1191+
1192+ requestId = data . TryGetProperty ( "requestId" , out var rid ) ? rid . GetString ( ) : null ;
1193+ var toolCallId = data . TryGetProperty ( "toolCallId" , out var tcid ) ? tcid . GetString ( ) : null ;
1194+ var toolName = data . TryGetProperty ( "toolName" , out var tn ) ? tn . GetString ( ) : null ;
1195+
1196+ if ( requestId == null || toolName == null ) return ;
1197+
1198+ var session = GetSession ( sessionId ) ;
1199+ if ( session == null ) return ;
1200+
1201+ ToolResultObject resultObj ;
1202+ if ( session . GetTool ( toolName ) is not { } tool )
1203+ {
1204+ resultObj = new ToolResultObject
1205+ {
1206+ TextResultForLlm = $ "Tool '{ toolName } ' is not supported.",
1207+ ResultType = "failure" ,
1208+ Error = $ "tool '{ toolName } ' not supported"
1209+ } ;
1210+ }
1211+ else
1212+ {
1213+ try
1214+ {
1215+ var arguments = data . TryGetProperty ( "arguments" , out var args ) ? ( object ? ) args : null ;
1216+
1217+ var invocation = new ToolInvocation
1218+ {
1219+ SessionId = sessionId ,
1220+ ToolCallId = toolCallId ?? "" ,
1221+ ToolName = toolName ,
1222+ Arguments = arguments
1223+ } ;
1224+
1225+ var aiFunctionArgs = new AIFunctionArguments
1226+ {
1227+ Context = new Dictionary < object , object ? >
1228+ {
1229+ [ typeof ( ToolInvocation ) ] = invocation
1230+ }
1231+ } ;
1232+
1233+ if ( arguments is JsonElement { ValueKind : JsonValueKind . Object } incomingJsonArgs )
1234+ {
1235+ foreach ( var prop in incomingJsonArgs . EnumerateObject ( ) )
1236+ {
1237+ aiFunctionArgs [ prop . Name ] = prop . Value ;
1238+ }
1239+ }
1240+
1241+ var result = await tool . InvokeAsync ( aiFunctionArgs ) ;
1242+
1243+ resultObj = result is ToolResultAIContent trac ? trac . Result : new ToolResultObject
1244+ {
1245+ ResultType = "success" ,
1246+ TextResultForLlm = result is JsonElement { ValueKind : JsonValueKind . String } je
1247+ ? je . GetString ( ) !
1248+ : JsonSerializer . Serialize ( result , tool . JsonSerializerOptions . GetTypeInfo ( typeof ( object ) ) ) ,
1249+ } ;
1250+ }
1251+ catch ( Exception ex )
1252+ {
1253+ resultObj = new ToolResultObject
1254+ {
1255+ TextResultForLlm = "Invoking this tool produced an error. Detailed information is not available." ,
1256+ ResultType = "failure" ,
1257+ Error = ex . Message
1258+ } ;
1259+ }
1260+ }
1261+
1262+ if ( _jsonRpc is { } rpc )
1263+ {
1264+ await InvokeRpcAsync < JsonElement > ( rpc , "session.tools.handlePendingToolCall" ,
1265+ [ new HandlePendingToolCallRequest ( sessionId , requestId , Result : resultObj ) ] , CancellationToken . None ) ;
1266+ }
1267+ }
1268+ catch ( Exception )
1269+ {
1270+ try
1271+ {
1272+ if ( _jsonRpc is { } rpc && requestId != null )
1273+ {
1274+ await InvokeRpcAsync < JsonElement > ( rpc , "session.tools.handlePendingToolCall" ,
1275+ [ new HandlePendingToolCallRequest ( sessionId , requestId , Error : "Internal error handling tool call" ) ] ,
1276+ CancellationToken . None ) ;
1277+ }
1278+ }
1279+ catch { /* Connection may be closed */ }
1280+ }
1281+ }
1282+
1283+ private async Task HandlePermissionRequestedEventAsync ( string sessionId , JsonElement eventJson )
1284+ {
1285+ string ? requestId = null ;
1286+ try
1287+ {
1288+ if ( ! eventJson . TryGetProperty ( "data" , out var data ) ) return ;
1289+
1290+ requestId = data . TryGetProperty ( "requestId" , out var rid ) ? rid . GetString ( ) : null ;
1291+ if ( requestId == null ) return ;
1292+
1293+ var session = GetSession ( sessionId ) ;
1294+ if ( session == null ) return ;
1295+
1296+ if ( ! data . TryGetProperty ( "permissionRequest" , out var permissionRequest ) ) return ;
1297+
1298+ var result = await session . HandlePermissionRequestAsync ( permissionRequest ) ;
1299+
1300+ if ( _jsonRpc is { } rpc )
1301+ {
1302+ await InvokeRpcAsync < JsonElement > ( rpc , "session.permissions.handlePendingPermissionRequest" ,
1303+ [ new HandlePendingPermissionRequestRequest ( sessionId , requestId , result ) ] , CancellationToken . None ) ;
1304+ }
1305+ }
1306+ catch ( Exception )
1307+ {
1308+ try
1309+ {
1310+ if ( _jsonRpc is { } rpc && requestId != null )
1311+ {
1312+ var deniedResult = new PermissionRequestResult
1313+ {
1314+ Kind = PermissionRequestResultKind . DeniedCouldNotRequestFromUser
1315+ } ;
1316+ await InvokeRpcAsync < JsonElement > ( rpc , "session.permissions.handlePendingPermissionRequest" ,
1317+ [ new HandlePendingPermissionRequestRequest ( sessionId , requestId , deniedResult ) ] , CancellationToken . None ) ;
1318+ }
1319+ }
1320+ catch { /* Connection may be closed */ }
1321+ }
1322+ }
1323+
11761324 /// <summary>
11771325 /// Disposes the <see cref="CopilotClient"/> synchronously.
11781326 /// </summary>
@@ -1202,15 +1350,41 @@ private class RpcHandler(CopilotClient client)
12021350 {
12031351 public void OnSessionEvent ( string sessionId , JsonElement ? @event )
12041352 {
1353+ if ( @event == null ) return ;
1354+
1355+ // Extract event type for v3 broadcast handling
1356+ string ? eventType = null ;
1357+ if ( @event . Value . TryGetProperty ( "type" , out var typeProp ) )
1358+ {
1359+ eventType = typeProp . GetString ( ) ;
1360+ }
1361+
1362+ // external_tool.requested is not in the typed schema; intercept on v3, always skip deserialization
1363+ if ( eventType == "external_tool.requested" )
1364+ {
1365+ if ( client . _negotiatedProtocolVersion >= 3 )
1366+ {
1367+ _ = Task . Run ( ( ) => client . HandleExternalToolRequestedAsync ( sessionId , @event . Value ) ) ;
1368+ }
1369+ return ;
1370+ }
1371+
1372+ // Normal typed event dispatch
12051373 var session = client . GetSession ( sessionId ) ;
1206- if ( session != null && @event != null )
1374+ if ( session != null )
12071375 {
12081376 var evt = SessionEvent . FromJson ( @event . Value . GetRawText ( ) ) ;
12091377 if ( evt != null )
12101378 {
12111379 session . DispatchEvent ( evt ) ;
12121380 }
12131381 }
1382+
1383+ // v3: permission.requested - handle via RPC callback in addition to event dispatch
1384+ if ( client . _negotiatedProtocolVersion >= 3 && eventType == "permission.requested" )
1385+ {
1386+ _ = Task . Run ( ( ) => client . HandlePermissionRequestedEventAsync ( sessionId , @event . Value ) ) ;
1387+ }
12141388 }
12151389
12161390 public void OnSessionLifecycle ( string type , string sessionId , JsonElement ? metadata )
@@ -1486,6 +1660,17 @@ internal record UserInputRequestResponse(
14861660 internal record HooksInvokeResponse (
14871661 object ? Output ) ;
14881662
1663+ internal record HandlePendingToolCallRequest (
1664+ string SessionId ,
1665+ string RequestId ,
1666+ ToolResultObject ? Result = null ,
1667+ string ? Error = null ) ;
1668+
1669+ internal record HandlePendingPermissionRequestRequest (
1670+ string SessionId ,
1671+ string RequestId ,
1672+ PermissionRequestResult Result ) ;
1673+
14891674 /// <summary>Trace source that forwards all logs to the ILogger.</summary>
14901675 internal sealed class LoggerTraceSource : TraceSource
14911676 {
@@ -1575,6 +1760,8 @@ private static LogLevel MapLevel(TraceEventType eventType)
15751760 [ JsonSerializable ( typeof ( DeleteSessionRequest ) ) ]
15761761 [ JsonSerializable ( typeof ( DeleteSessionResponse ) ) ]
15771762 [ JsonSerializable ( typeof ( GetLastSessionIdResponse ) ) ]
1763+ [ JsonSerializable ( typeof ( HandlePendingPermissionRequestRequest ) ) ]
1764+ [ JsonSerializable ( typeof ( HandlePendingToolCallRequest ) ) ]
15781765 [ JsonSerializable ( typeof ( HooksInvokeResponse ) ) ]
15791766 [ JsonSerializable ( typeof ( ListSessionsRequest ) ) ]
15801767 [ JsonSerializable ( typeof ( ListSessionsResponse ) ) ]
0 commit comments