Skip to content

Commit 31d4acf

Browse files
Update to latest runtime and add multiclient tools/permissions support to Python/Go/.NET (protocol v3)
1 parent b0752e5 commit 31d4acf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+4617
-1238
lines changed

dotnet/src/Client.cs

Lines changed: 8 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)