Skip to content

Commit 396e8b3

Browse files
Add v2 protocol backward compatibility adapters (#706)
1 parent 1653812 commit 396e8b3

File tree

5 files changed

+578
-62
lines changed

5 files changed

+578
-62
lines changed

dotnet/src/Client.cs

Lines changed: 120 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ namespace GitHub.Copilot.SDK;
5454
/// </example>
5555
public 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 supports versions {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,96 @@ 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+
?? throw new ArgumentException($"Unknown session {sessionId}");
1351+
1352+
try
1353+
{
1354+
var result = await session.HandlePermissionRequestAsync(permissionRequest);
1355+
return new PermissionRequestResponseV2(result);
1356+
}
1357+
catch (Exception)
1358+
{
1359+
return new PermissionRequestResponseV2(new PermissionRequestResult
1360+
{
1361+
Kind = PermissionRequestResultKind.DeniedCouldNotRequestFromUser
1362+
});
1363+
}
1364+
}
12601365
}
12611366

12621367
private class Connection(
@@ -1376,6 +1481,13 @@ internal record UserInputRequestResponse(
13761481
internal record HooksInvokeResponse(
13771482
object? Output);
13781483

1484+
// Protocol v2 backward-compatibility response types
1485+
internal record ToolCallResponseV2(
1486+
ToolResultObject Result);
1487+
1488+
internal record PermissionRequestResponseV2(
1489+
PermissionRequestResult Result);
1490+
13791491
/// <summary>Trace source that forwards all logs to the ILogger.</summary>
13801492
internal sealed class LoggerTraceSource : TraceSource
13811493
{
@@ -1469,11 +1581,13 @@ private static LogLevel MapLevel(TraceEventType eventType)
14691581
[JsonSerializable(typeof(ListSessionsRequest))]
14701582
[JsonSerializable(typeof(ListSessionsResponse))]
14711583
[JsonSerializable(typeof(PermissionRequestResult))]
1584+
[JsonSerializable(typeof(PermissionRequestResponseV2))]
14721585
[JsonSerializable(typeof(ProviderConfig))]
14731586
[JsonSerializable(typeof(ResumeSessionRequest))]
14741587
[JsonSerializable(typeof(ResumeSessionResponse))]
14751588
[JsonSerializable(typeof(SessionMetadata))]
14761589
[JsonSerializable(typeof(SystemMessageConfig))]
1590+
[JsonSerializable(typeof(ToolCallResponseV2))]
14771591
[JsonSerializable(typeof(ToolDefinition))]
14781592
[JsonSerializable(typeof(ToolResultAIContent))]
14791593
[JsonSerializable(typeof(ToolResultObject))]

0 commit comments

Comments
 (0)