@@ -61,6 +61,7 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
6161 private bool _disposed ;
6262 private readonly int ? _optionsPort ;
6363 private readonly string ? _optionsHost ;
64+ private int ? _actualPort ;
6465 private List < ModelInfo > ? _modelsCache ;
6566 private readonly SemaphoreSlim _modelsCacheLock = new ( 1 , 1 ) ;
6667 private readonly List < Action < SessionLifecycleEvent > > _lifecycleHandlers = [ ] ;
@@ -80,6 +81,11 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
8081 ? throw new ObjectDisposedException ( nameof ( CopilotClient ) )
8182 : _rpc ?? throw new InvalidOperationException ( "Client is not started. Call StartAsync first." ) ;
8283
84+ /// <summary>
85+ /// Gets the actual TCP port the CLI server is listening on, if using TCP transport.
86+ /// </summary>
87+ public int ? ActualPort => _actualPort ;
88+
8389 /// <summary>
8490 /// Creates a new instance of <see cref="CopilotClient"/>.
8591 /// </summary>
@@ -191,12 +197,14 @@ async Task<Connection> StartCoreAsync(CancellationToken ct)
191197 if ( _optionsHost is not null && _optionsPort is not null )
192198 {
193199 // External server (TCP)
200+ _actualPort = _optionsPort ;
194201 result = ConnectToServerAsync ( null , _optionsHost , _optionsPort , null , ct ) ;
195202 }
196203 else
197204 {
198205 // Child process (stdio or TCP)
199206 var ( cliProcess , portOrNull , stderrBuffer ) = await StartCliServerAsync ( _options , _logger , ct ) ;
207+ _actualPort = portOrNull ;
200208 result = ConnectToServerAsync ( cliProcess , portOrNull is null ? null : "localhost" , portOrNull , stderrBuffer , ct ) ;
201209 }
202210
@@ -1129,8 +1137,6 @@ private async Task<Connection> ConnectToServerAsync(Process? cliProcess, string?
11291137 var handler = new RpcHandler ( this ) ;
11301138 rpc . AddLocalRpcMethod ( "session.event" , handler . OnSessionEvent ) ;
11311139 rpc . AddLocalRpcMethod ( "session.lifecycle" , handler . OnSessionLifecycle ) ;
1132- rpc . AddLocalRpcMethod ( "tool.call" , handler . OnToolCall ) ;
1133- rpc . AddLocalRpcMethod ( "permission.request" , handler . OnPermissionRequest ) ;
11341140 rpc . AddLocalRpcMethod ( "userInput.request" , handler . OnUserInputRequest ) ;
11351141 rpc . AddLocalRpcMethod ( "hooks.invoke" , handler . OnHooksInvoke ) ;
11361142 rpc . StartListening ( ) ;
@@ -1231,116 +1237,6 @@ public void OnSessionLifecycle(string type, string sessionId, JsonElement? metad
12311237 client . DispatchLifecycleEvent ( evt ) ;
12321238 }
12331239
1234- public async Task < ToolCallResponse > OnToolCall ( string sessionId ,
1235- string toolCallId ,
1236- string toolName ,
1237- object ? arguments )
1238- {
1239- var session = client . GetSession ( sessionId ) ?? throw new ArgumentException ( $ "Unknown session { sessionId } ") ;
1240- if ( session . GetTool ( toolName ) is not { } tool )
1241- {
1242- return new ToolCallResponse ( new ToolResultObject
1243- {
1244- TextResultForLlm = $ "Tool '{ toolName } ' is not supported.",
1245- ResultType = "failure" ,
1246- Error = $ "tool '{ toolName } ' not supported"
1247- } ) ;
1248- }
1249-
1250- try
1251- {
1252- var invocation = new ToolInvocation
1253- {
1254- SessionId = sessionId ,
1255- ToolCallId = toolCallId ,
1256- ToolName = toolName ,
1257- Arguments = arguments
1258- } ;
1259-
1260- // Map args from JSON into AIFunction format
1261- var aiFunctionArgs = new AIFunctionArguments
1262- {
1263- Context = new Dictionary < object , object ? >
1264- {
1265- // Allow recipient to access the raw ToolInvocation if they want, e.g., to get SessionId
1266- // This is an alternative to using MEAI's ConfigureParameterBinding, which we can't use
1267- // because we're not the ones producing the AIFunction.
1268- [ typeof ( ToolInvocation ) ] = invocation
1269- }
1270- } ;
1271-
1272- if ( arguments is not null )
1273- {
1274- if ( arguments is not JsonElement incomingJsonArgs )
1275- {
1276- throw new InvalidOperationException ( $ "Incoming arguments must be a { nameof ( JsonElement ) } ; received { arguments . GetType ( ) . Name } ") ;
1277- }
1278-
1279- foreach ( var prop in incomingJsonArgs . EnumerateObject ( ) )
1280- {
1281- // MEAI will deserialize the JsonElement value respecting the delegate's parameter types
1282- aiFunctionArgs [ prop . Name ] = prop . Value ;
1283- }
1284- }
1285-
1286- var result = await tool . InvokeAsync ( aiFunctionArgs ) ;
1287-
1288- // If the function returns a ToolResultObject, use it directly; otherwise, wrap the result
1289- // This lets the developer provide BinaryResult, SessionLog, etc. if they deal with that themselves
1290- var toolResultObject = result is ToolResultAIContent trac ? trac . Result : new ToolResultObject
1291- {
1292- ResultType = "success" ,
1293-
1294- // In most cases, result will already have been converted to JsonElement by the AIFunction.
1295- // We special-case string for consistency with our Node/Python/Go clients.
1296- // TODO: I don't think it's right to special-case string here, and all the clients should
1297- // always serialize the result to JSON (otherwise what stringification is going to happen?
1298- // something we don't control? an error?)
1299- TextResultForLlm = result is JsonElement { ValueKind : JsonValueKind . String } je
1300- ? je . GetString ( ) !
1301- : JsonSerializer . Serialize ( result , tool . JsonSerializerOptions . GetTypeInfo ( typeof ( object ) ) ) ,
1302- } ;
1303- return new ToolCallResponse ( toolResultObject ) ;
1304- }
1305- catch ( Exception ex )
1306- {
1307- return new ToolCallResponse ( new ( )
1308- {
1309- // TODO: We should offer some way to control whether or not to expose detailed exception information to the LLM.
1310- // For security, the default must be false, but developers can opt into allowing it.
1311- TextResultForLlm = $ "Invoking this tool produced an error. Detailed information is not available.",
1312- ResultType = "failure" ,
1313- Error = ex . Message
1314- } ) ;
1315- }
1316- }
1317-
1318- public async Task < PermissionRequestResponse > OnPermissionRequest ( string sessionId , JsonElement permissionRequest )
1319- {
1320- var session = client . GetSession ( sessionId ) ;
1321- if ( session == null )
1322- {
1323- return new PermissionRequestResponse ( new PermissionRequestResult
1324- {
1325- Kind = PermissionRequestResultKind . DeniedCouldNotRequestFromUser
1326- } ) ;
1327- }
1328-
1329- try
1330- {
1331- var result = await session . HandlePermissionRequestAsync ( permissionRequest ) ;
1332- return new PermissionRequestResponse ( result ) ;
1333- }
1334- catch
1335- {
1336- // If permission handler fails, deny the permission
1337- return new PermissionRequestResponse ( new PermissionRequestResult
1338- {
1339- Kind = PermissionRequestResultKind . DeniedCouldNotRequestFromUser
1340- } ) ;
1341- }
1342- }
1343-
13441240 public async Task < UserInputRequestResponse > OnUserInputRequest ( string sessionId , string question , List < string > ? choices = null , bool ? allowFreeform = null )
13451241 {
13461242 var session = client . GetSession ( sessionId ) ?? throw new ArgumentException ( $ "Unknown session { sessionId } ") ;
@@ -1473,12 +1369,6 @@ internal record ListSessionsRequest(
14731369 internal record ListSessionsResponse (
14741370 List < SessionMetadata > Sessions ) ;
14751371
1476- internal record ToolCallResponse (
1477- ToolResultObject ? Result ) ;
1478-
1479- internal record PermissionRequestResponse (
1480- PermissionRequestResult Result ) ;
1481-
14821372 internal record UserInputRequestResponse (
14831373 string Answer ,
14841374 bool WasFreeform ) ;
@@ -1578,14 +1468,12 @@ private static LogLevel MapLevel(TraceEventType eventType)
15781468 [ JsonSerializable ( typeof ( HooksInvokeResponse ) ) ]
15791469 [ JsonSerializable ( typeof ( ListSessionsRequest ) ) ]
15801470 [ JsonSerializable ( typeof ( ListSessionsResponse ) ) ]
1581- [ JsonSerializable ( typeof ( PermissionRequestResponse ) ) ]
15821471 [ JsonSerializable ( typeof ( PermissionRequestResult ) ) ]
15831472 [ JsonSerializable ( typeof ( ProviderConfig ) ) ]
15841473 [ JsonSerializable ( typeof ( ResumeSessionRequest ) ) ]
15851474 [ JsonSerializable ( typeof ( ResumeSessionResponse ) ) ]
15861475 [ JsonSerializable ( typeof ( SessionMetadata ) ) ]
15871476 [ JsonSerializable ( typeof ( SystemMessageConfig ) ) ]
1588- [ JsonSerializable ( typeof ( ToolCallResponse ) ) ]
15891477 [ JsonSerializable ( typeof ( ToolDefinition ) ) ]
15901478 [ JsonSerializable ( typeof ( ToolResultAIContent ) ) ]
15911479 [ JsonSerializable ( typeof ( ToolResultObject ) ) ]
0 commit comments