Skip to content

Commit 033edcd

Browse files
Add v2 protocol backward compatibility adapters
Register v2-style tool.call and permission.request JSON-RPC request handlers on all 4 SDKs so that SDK clients written against the v3 API still work at runtime when connected to a v2 CLI server. - Change protocol version negotiation from strict equality to range-based [MIN_PROTOCOL_VERSION(2), SDK_PROTOCOL_VERSION(3)] - Always register v2 handlers unconditionally (v3 servers never send them) - Node/Python/Go/.NET all implement handleToolCallRequestV2 and handlePermissionRequestV2 adapters that invoke the same user-facing tool and permission handlers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1653812 commit 033edcd

File tree

5 files changed

+581
-62
lines changed

5 files changed

+581
-62
lines changed

dotnet/src/Client.cs

Lines changed: 126 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 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

Comments
 (0)