Skip to content

Commit 924ed04

Browse files
committed
Make the .NET library NativeAOT compatible
- Enabled AOT analyzers - Disabled STJ reflection in the test project (to help vet NAOT correctness) - Use source generation for all types that may be serialized - Remove all use of anonymous types - Removed <autogenerated/> from the source generated code, as it was suppressing the analyzers - Added support for propagating StreamJsonRpc's tracing to the CopilotClient's ILogger. I used this for debugging and decided to leave it - Updated StreamJsonRpc to a newly published version on nuget to pick up NativeAOT fixes - Cleaned up some formatting in the session types generator, in particular using a file-scoped namespace and removing the top-level indentation
1 parent cb80f83 commit 924ed04

File tree

9 files changed

+1251
-1014
lines changed

9 files changed

+1251
-1014
lines changed

dotnet/src/Client.cs

Lines changed: 113 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ namespace GitHub.Copilot.SDK;
4949
/// await session.SendAsync(new MessageOptions { Prompt = "Hello!" });
5050
/// </code>
5151
/// </example>
52-
public class CopilotClient : IDisposable, IAsyncDisposable
52+
public partial class CopilotClient : IDisposable, IAsyncDisposable
5353
{
5454
private readonly ConcurrentDictionary<string, CopilotSession> _sessions = new();
5555
private readonly CopilotClientOptions _options;
@@ -461,7 +461,7 @@ public async Task<PingResponse> PingAsync(string? message = null, CancellationTo
461461
var connection = await EnsureConnectedAsync(cancellationToken);
462462

463463
return await connection.Rpc.InvokeWithCancellationAsync<PingResponse>(
464-
"ping", [new { message }], cancellationToken);
464+
"ping", [new PingRequest { Message = message }], cancellationToken);
465465
}
466466

467467
/// <summary>
@@ -510,7 +510,7 @@ public async Task DeleteSessionAsync(string sessionId, CancellationToken cancell
510510
var connection = await EnsureConnectedAsync(cancellationToken);
511511

512512
var response = await connection.Rpc.InvokeWithCancellationAsync<DeleteSessionResponse>(
513-
"session.delete", [new { sessionId }], cancellationToken);
513+
"session.delete", [new DeleteSessionRequest(sessionId)], cancellationToken);
514514

515515
if (!response.Success)
516516
{
@@ -560,7 +560,7 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
560560
{
561561
var expectedVersion = SdkProtocolVersion.GetVersion();
562562
var pingResponse = await connection.Rpc.InvokeWithCancellationAsync<PingResponse>(
563-
"ping", [new { message = (string?)null }], cancellationToken);
563+
"ping", [new PingRequest()], cancellationToken);
564564

565565
if (!pingResponse.ProtocolVersion.HasValue)
566566
{
@@ -710,23 +710,45 @@ private async Task<Connection> ConnectToServerAsync(Process? cliProcess, string?
710710
outputStream = networkStream;
711711
}
712712

713-
var rpc = new JsonRpc(new HeaderDelimitedMessageHandler(outputStream, inputStream, CreateFormatter()));
714-
rpc.AddLocalRpcTarget(new RpcHandler(this));
713+
var rpc = new JsonRpc(new HeaderDelimitedMessageHandler(
714+
outputStream,
715+
inputStream,
716+
CreateSystemTextJsonFormatter()))
717+
{
718+
TraceSource = new LoggerTraceSource(_logger),
719+
};
720+
721+
var handler = new RpcHandler(this);
722+
rpc.AddLocalRpcMethod("session.event", handler.OnSessionEvent);
723+
rpc.AddLocalRpcMethod("tool.call", handler.OnToolCall);
724+
rpc.AddLocalRpcMethod("permission.request", handler.OnPermissionRequest);
715725
rpc.StartListening();
716726
return new Connection(rpc, cliProcess, tcpClient, networkStream);
717727
}
718728

719-
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Using the Json source generator.")]
720-
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Using the Json source generator.")]
721-
static IJsonRpcMessageFormatter CreateFormatter()
729+
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Using happy path from https://microsoft.github.io/vs-streamjsonrpc/docs/nativeAOT.html")]
730+
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Using happy path from https://microsoft.github.io/vs-streamjsonrpc/docs/nativeAOT.html")]
731+
private static SystemTextJsonFormatter CreateSystemTextJsonFormatter() =>
732+
new SystemTextJsonFormatter() { JsonSerializerOptions = SerializerOptionsForMessageFormatter };
733+
734+
private static JsonSerializerOptions SerializerOptionsForMessageFormatter { get; } = CreateSerializerOptions();
735+
736+
private static JsonSerializerOptions CreateSerializerOptions()
722737
{
723738
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
724739
{
725740
AllowOutOfOrderMetadataProperties = true,
726741
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
727742
};
728743

729-
return new SystemTextJsonFormatter() { JsonSerializerOptions = options };
744+
options.TypeInfoResolverChain.Add(ClientJsonContext.Default);
745+
options.TypeInfoResolverChain.Add(TypesJsonContext.Default);
746+
options.TypeInfoResolverChain.Add(CopilotSession.SessionJsonContext.Default);
747+
options.TypeInfoResolverChain.Add(SessionEventsJsonContext.Default);
748+
749+
options.MakeReadOnly();
750+
751+
return options;
730752
}
731753

732754
internal CopilotSession? GetSession(string sessionId) =>
@@ -759,9 +781,7 @@ public async ValueTask DisposeAsync()
759781

760782
private class RpcHandler(CopilotClient client)
761783
{
762-
[JsonRpcMethod("session.event")]
763-
public void OnSessionEvent(string sessionId,
764-
JsonElement? @event)
784+
public void OnSessionEvent(string sessionId, JsonElement? @event)
765785
{
766786
var session = client.GetSession(sessionId);
767787
if (session != null && @event != null)
@@ -774,7 +794,6 @@ public void OnSessionEvent(string sessionId,
774794
}
775795
}
776796

777-
[JsonRpcMethod("tool.call")]
778797
public async Task<ToolCallResponse> OnToolCall(string sessionId,
779798
string toolCallId,
780799
string toolName,
@@ -847,7 +866,7 @@ public async Task<ToolCallResponse> OnToolCall(string sessionId,
847866
// something we don't control? an error?)
848867
TextResultForLlm = result is JsonElement { ValueKind: JsonValueKind.String } je
849868
? je.GetString()!
850-
: JsonSerializer.Serialize(result, tool.JsonSerializerOptions),
869+
: JsonSerializer.Serialize(result, tool.JsonSerializerOptions.GetTypeInfo(typeof(object))),
851870
};
852871
return new ToolCallResponse(toolResultObject);
853872
}
@@ -864,7 +883,6 @@ public async Task<ToolCallResponse> OnToolCall(string sessionId,
864883
}
865884
}
866885

867-
[JsonRpcMethod("permission.request")]
868886
public async Task<PermissionRequestResponse> OnPermissionRequest(string sessionId, JsonElement permissionRequest)
869887
{
870888
var session = client.GetSession(sessionId);
@@ -915,7 +933,7 @@ public static string Escape(string arg)
915933
}
916934

917935
// Request/Response types for RPC
918-
private record CreateSessionRequest(
936+
internal record CreateSessionRequest(
919937
string? Model,
920938
string? SessionId,
921939
List<ToolDefinition>? Tools,
@@ -931,7 +949,7 @@ private record CreateSessionRequest(
931949
List<string>? SkillDirectories,
932950
List<string>? DisabledSkills);
933951

934-
private record ToolDefinition(
952+
internal record ToolDefinition(
935953
string Name,
936954
string? Description,
937955
JsonElement Parameters /* JSON schema */)
@@ -940,10 +958,10 @@ public static ToolDefinition FromAIFunction(AIFunction function)
940958
=> new ToolDefinition(function.Name, function.Description, function.JsonSchema);
941959
}
942960

943-
private record CreateSessionResponse(
961+
internal record CreateSessionResponse(
944962
string SessionId);
945963

946-
private record ResumeSessionRequest(
964+
internal record ResumeSessionRequest(
947965
string SessionId,
948966
List<ToolDefinition>? Tools,
949967
ProviderConfig? Provider,
@@ -954,24 +972,93 @@ private record ResumeSessionRequest(
954972
List<string>? SkillDirectories,
955973
List<string>? DisabledSkills);
956974

957-
private record ResumeSessionResponse(
975+
internal record ResumeSessionResponse(
958976
string SessionId);
959977

960-
private record GetLastSessionIdResponse(
978+
internal record GetLastSessionIdResponse(
961979
string? SessionId);
962980

963-
private record DeleteSessionResponse(
981+
internal record DeleteSessionRequest(
982+
string SessionId);
983+
984+
internal record DeleteSessionResponse(
964985
bool Success,
965986
string? Error);
966987

967-
private record ListSessionsResponse(
988+
internal record ListSessionsResponse(
968989
List<SessionMetadata> Sessions);
969990

970-
private record ToolCallResponse(
991+
internal record ToolCallResponse(
971992
ToolResultObject? Result);
972993

973-
private record PermissionRequestResponse(
994+
internal record PermissionRequestResponse(
974995
PermissionRequestResult Result);
996+
997+
/// <summary>Trace source that forwards all logs to the ILogger.</summary>
998+
internal sealed class LoggerTraceSource : TraceSource
999+
{
1000+
public LoggerTraceSource(ILogger logger) : base(nameof(LoggerTraceSource), SourceLevels.All)
1001+
{
1002+
Listeners.Clear();
1003+
Listeners.Add(new LoggerTraceListener(logger));
1004+
}
1005+
1006+
private sealed class LoggerTraceListener(ILogger logger) : TraceListener
1007+
{
1008+
public override void TraceEvent(TraceEventCache? eventCache, string source, TraceEventType eventType, int id, string? message) =>
1009+
logger.Log(MapLevel(eventType), "[{Source}] {Message}", source, message);
1010+
1011+
public override void TraceEvent(TraceEventCache? eventCache, string source, TraceEventType eventType, int id, string? format, params object?[]? args) =>
1012+
logger.Log(MapLevel(eventType), "[{Source}] {Message}", source, args is null || args.Length == 0 ? format : string.Format(format ?? "", args));
1013+
1014+
public override void TraceData(TraceEventCache? eventCache, string source, TraceEventType eventType, int id, object? data) =>
1015+
logger.Log(MapLevel(eventType), "[{Source}] {Data}", source, data);
1016+
1017+
public override void TraceData(TraceEventCache? eventCache, string source, TraceEventType eventType, int id, params object?[]? data) =>
1018+
logger.Log(MapLevel(eventType), "[{Source}] {Data}", source, data is null ? null : string.Join(", ", data));
1019+
1020+
public override void Write(string? message) =>
1021+
logger.LogTrace("{Message}", message);
1022+
1023+
public override void WriteLine(string? message) =>
1024+
logger.LogTrace("{Message}", message);
1025+
1026+
private static LogLevel MapLevel(TraceEventType eventType) => eventType switch
1027+
{
1028+
TraceEventType.Critical => LogLevel.Critical,
1029+
TraceEventType.Error => LogLevel.Error,
1030+
TraceEventType.Warning => LogLevel.Warning,
1031+
TraceEventType.Information => LogLevel.Information,
1032+
TraceEventType.Verbose => LogLevel.Debug,
1033+
_ => LogLevel.Trace
1034+
};
1035+
}
1036+
}
1037+
1038+
[JsonSourceGenerationOptions(
1039+
JsonSerializerDefaults.Web,
1040+
AllowOutOfOrderMetadataProperties = true,
1041+
NumberHandling = JsonNumberHandling.AllowReadingFromString,
1042+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
1043+
[JsonSerializable(typeof(CreateSessionRequest))]
1044+
[JsonSerializable(typeof(CreateSessionResponse))]
1045+
[JsonSerializable(typeof(CustomAgentConfig))]
1046+
[JsonSerializable(typeof(DeleteSessionRequest))]
1047+
[JsonSerializable(typeof(DeleteSessionResponse))]
1048+
[JsonSerializable(typeof(GetLastSessionIdResponse))]
1049+
[JsonSerializable(typeof(ListSessionsResponse))]
1050+
[JsonSerializable(typeof(PermissionRequestResponse))]
1051+
[JsonSerializable(typeof(PermissionRequestResult))]
1052+
[JsonSerializable(typeof(ProviderConfig))]
1053+
[JsonSerializable(typeof(ResumeSessionRequest))]
1054+
[JsonSerializable(typeof(ResumeSessionResponse))]
1055+
[JsonSerializable(typeof(SessionMetadata))]
1056+
[JsonSerializable(typeof(SystemMessageConfig))]
1057+
[JsonSerializable(typeof(ToolCallResponse))]
1058+
[JsonSerializable(typeof(ToolDefinition))]
1059+
[JsonSerializable(typeof(ToolResultAIContent))]
1060+
[JsonSerializable(typeof(ToolResultObject))]
1061+
internal partial class ClientJsonContext : JsonSerializerContext;
9751062
}
9761063

9771064
// Must inherit from AIContent as a signal to MEAI to avoid JSON-serializing the

0 commit comments

Comments
 (0)