Skip to content

Commit 5b2d7cd

Browse files
Update SDK snapshot for Copilot CLI 1.0.92-2
1 parent 9b28c94 commit 5b2d7cd

13 files changed

Lines changed: 1107 additions & 12 deletions

‎dotnet/README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,13 @@ Abort the currently processing message in this session.
505505

506506
Get all events/messages from this session.
507507

508+
Live notifications and history deserialization select source-generated metadata for the
509+
received event type rather than initializing every event type on the first event.
510+
History parsing uses a bounded stack or pooled UTF-8 buffer to avoid an extra
511+
reader-scoping pass; common event-type selection does not allocate a discriminator string.
512+
Unknown or missing event types retain the base `SessionEvent` fallback; serialization
513+
and malformed-event validation are unchanged.
514+
508515
##### `DisposeAsync(): ValueTask`
509516

510517
Close the session and release in-memory resources. Session data on disk is preserved — the conversation can be resumed later via `ResumeSessionAsync()`. To permanently delete session data, use `client.DeleteSessionAsync()`.

‎dotnet/src/Client.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2864,6 +2864,8 @@ private static JsonSerializerOptions CreateSerializerOptions()
28642864
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
28652865
};
28662866

2867+
options.Converters.Add(SessionEventJsonConverter.Default);
2868+
options.TypeInfoResolverChain.Add(SessionEventJsonTypeInfoResolver.Default);
28672869
options.TypeInfoResolverChain.Add(ClientJsonContext.Default);
28682870
options.TypeInfoResolverChain.Add(TypesJsonContext.Default);
28692871
options.TypeInfoResolverChain.Add(CopilotSession.SessionJsonContext.Default);

‎dotnet/src/Generated/SessionEvents.cs‎

Lines changed: 512 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
using System.Buffers;
6+
using System.Text;
7+
using System.Text.Json;
8+
using System.Text.Json.Serialization.Metadata;
9+
#if !NETSTANDARD2_0
10+
using System.Text.Unicode;
11+
#endif
12+
13+
namespace GitHub.Copilot;
14+
15+
internal sealed partial class SessionEventJsonConverter
16+
{
17+
private static readonly Encoding StrictUtf8 = new UTF8Encoding(false, true);
18+
19+
internal static SessionEvent Deserialize(string json)
20+
{
21+
#if !NETSTANDARD2_0
22+
const int StackallocThreshold = 512;
23+
#endif
24+
ArgumentNullException.ThrowIfNull(json);
25+
26+
byte[]? rented = null;
27+
Span<byte> buffer =
28+
#if !NETSTANDARD2_0
29+
json.Length <= StackallocThreshold / 3 ? stackalloc byte[StackallocThreshold] :
30+
#endif
31+
rented = ArrayPool<byte>.Shared.Rent(GetUtf8ByteCount(json));
32+
try
33+
{
34+
#if NETSTANDARD2_0
35+
int byteCount = StrictUtf8.GetBytes(json, 0, json.Length, rented, 0);
36+
#else
37+
var status = Utf8.FromUtf16(json.AsSpan(), buffer, out _, out int byteCount, replaceInvalidSequences: false);
38+
if (status != OperationStatus.Done)
39+
{
40+
throw new ArgumentException($"Cannot transcode UTF-16 JSON text to UTF-8 ({status}).", nameof(json));
41+
}
42+
#endif
43+
ReadOnlySpan<byte> utf8 = buffer.Slice(0, byteCount);
44+
var reader = new Utf8JsonReader(utf8);
45+
46+
// Select metadata without entering the converter: reader-based nested deserialization
47+
// would scan the entire object again to scope its input before parsing it.
48+
JsonTypeInfo typeInfo;
49+
try
50+
{
51+
reader.Read();
52+
typeInfo = ReadEventTypeInfo(ref reader);
53+
}
54+
catch (JsonException ex) when (ex.GetType() != typeof(JsonException))
55+
{
56+
// Match JsonSerializer's normalization of reader errors.
57+
throw new JsonException(ex.Message, ex.Path, ex.LineNumber, ex.BytePositionInLine, ex);
58+
}
59+
return ToSessionEvent(JsonSerializer.Deserialize(utf8, typeInfo));
60+
}
61+
finally
62+
{
63+
if (rented is not null)
64+
{
65+
buffer.Clear();
66+
ArrayPool<byte>.Shared.Return(rented);
67+
}
68+
}
69+
}
70+
71+
private static int GetUtf8ByteCount(string json)
72+
{
73+
try
74+
{
75+
return StrictUtf8.GetByteCount(json);
76+
}
77+
catch (EncoderFallbackException ex)
78+
{
79+
throw new ArgumentException("Cannot transcode invalid UTF-16 JSON text to UTF-8.", nameof(json), ex);
80+
}
81+
}
82+
83+
private static string ReadString(ref Utf8JsonReader reader)
84+
{
85+
try
86+
{
87+
return reader.GetString()!;
88+
}
89+
catch (InvalidOperationException ex)
90+
{
91+
throw new JsonException(ex.Message, ex);
92+
}
93+
}
94+
}

‎dotnet/test/Unit/JsonRpcTests.cs‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,15 @@ public async Task JsonRpc_Malformed_Session_Event_Does_Not_Block_Unknown_Or_Know
258258
var malformed = CreateNotificationFrame(
259259
"session.event",
260260
"""{"sessionId":"session-1","event":42}""");
261+
var malformedDiscriminator = CreateNotificationFrame(
262+
"session.event",
263+
"""{"sessionId":"session-1","event":{"type":false}}""");
264+
var duplicateDiscriminator = CreateNotificationFrame(
265+
"session.event",
266+
"""{"sessionId":"session-1","event":{"type":"session.idle","data":{},"type":"session.idle"}}""");
267+
var unexpectedMetadata = CreateNotificationFrame(
268+
"session.event",
269+
"""{"sessionId":"session-1","event":{"type":"future.event","$id":"1"}}""");
261270
var unknown = CreateNotificationFrame(
262271
"session.event",
263272
"""
@@ -291,7 +300,8 @@ public async Task JsonRpc_Malformed_Session_Event_Does_Not_Block_Unknown_Or_Know
291300
}
292301
""");
293302

294-
using var receiveStream = new MemoryStream(CombineFrames([malformed, unknown, known]));
303+
using var receiveStream = new MemoryStream(CombineFrames(
304+
[malformed, malformedDiscriminator, duplicateDiscriminator, unexpectedMetadata, unknown, known]));
295305
using var rpc = new JsonRpcReflection(Stream.Null, receiveStream);
296306
var collector = new SessionEventCollector(expectedCount: 2);
297307
rpc.SetLocalRpcMethod("session.event", (Action<string, SessionEvent?>)collector.Handle);
@@ -455,11 +465,17 @@ private sealed class JsonRpcReflection : IDisposable
455465
private static readonly Type JsonRpcType =
456466
typeof(CopilotClient).Assembly.GetType("GitHub.Copilot.JsonRpc", throwOnError: true)!;
457467

458-
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
468+
private static readonly JsonSerializerOptions SerializerOptions = CreateSerializerOptions();
469+
470+
private static JsonSerializerOptions CreateSerializerOptions()
459471
{
460-
AllowOutOfOrderMetadataProperties = true,
461-
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
462-
};
472+
var property = typeof(CopilotClient).GetProperty(
473+
"SerializerOptionsForMessageFormatter",
474+
BindingFlags.Static | BindingFlags.NonPublic)!;
475+
var options = new JsonSerializerOptions((JsonSerializerOptions)property.GetValue(null)!);
476+
options.TypeInfoResolverChain.Add(new DefaultJsonTypeInfoResolver());
477+
return options;
478+
}
463479

464480
private readonly object _instance;
465481

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
using System.Reflection;
6+
using System.Text.Json;
7+
using System.Text.Json.Nodes;
8+
using System.Text.Json.Serialization;
9+
using System.Text.Json.Serialization.Metadata;
10+
using Xunit;
11+
12+
namespace GitHub.Copilot.Test.Unit;
13+
14+
public class SessionEventDispatchTests
15+
{
16+
public static IEnumerable<object[]> EventTypes =>
17+
typeof(SessionEvent).GetCustomAttributes<JsonDerivedTypeAttribute>()
18+
.Select(attribute => new object[] { attribute.TypeDiscriminator!, attribute.DerivedType });
19+
20+
public static IEnumerable<object[]> EventDiscriminators =>
21+
EventTypes.Select(eventType => new object[] { eventType[0] });
22+
23+
[Theory]
24+
[MemberData(nameof(EventTypes))]
25+
public void FromJson_DispatchesEveryRegisteredEvent(string discriminator, Type expectedType)
26+
{
27+
var json = $$"""
28+
{
29+
"data":null,
30+
"agentId":"child-agent",
31+
"ephemeral":true,
32+
"id":"11111111-1111-1111-1111-111111111111",
33+
"parentId":"22222222-2222-2222-2222-222222222222",
34+
"timestamp":"2026-09-30T00:00:00Z",
35+
"type":"{{discriminator}}"
36+
}
37+
""";
38+
39+
var result = SessionEvent.FromJson(json);
40+
41+
Assert.Equal(expectedType, result.GetType());
42+
Assert.Equal(discriminator, result.Type);
43+
Assert.Equal("child-agent", result.AgentId);
44+
Assert.True(result.Ephemeral);
45+
Assert.Equal(Guid.Parse("11111111-1111-1111-1111-111111111111"), result.Id);
46+
Assert.Equal(Guid.Parse("22222222-2222-2222-2222-222222222222"), result.ParentId);
47+
Assert.Equal(DateTimeOffset.Parse("2026-09-30T00:00:00Z"), result.Timestamp);
48+
}
49+
50+
[Theory]
51+
[MemberData(nameof(EventDiscriminators))]
52+
public void FromJson_DoesNotDispatchPartialDiscriminators(string discriminator)
53+
{
54+
var candidates = Enumerable.Range(0, discriminator.Length)
55+
.Select(index => discriminator[..index] + "?" + discriminator[(index + 1)..])
56+
.Prepend(discriminator[..^1])
57+
.Append(discriminator + "x");
58+
59+
foreach (var candidate in candidates)
60+
{
61+
var json = $$"""{"type":"{{candidate}}","agentId":"child","data":null}""";
62+
var actual = SessionEvent.FromJson(json);
63+
64+
Assert.IsType<SessionEvent>(actual);
65+
Assert.Equal("child", actual.AgentId);
66+
Assert.Equal(JsonSerializer.Deserialize(json, PolymorphicTypeInfo)?.ToJson(), actual.ToJson());
67+
}
68+
}
69+
70+
[Theory]
71+
[InlineData("""{"type":"user.message","data":{"content":"hello"}}""")]
72+
[InlineData("""{"data":{"nested":[{"type":"session.idle"}],"content":"hello"},"type":"user.message"}""")]
73+
[InlineData("""{"ty\u0070e":"user.message","data":{"content":"hello"}}""")]
74+
[InlineData("""{"type":"user\u002Emessage","data":{"content":"hello"}}""")]
75+
[InlineData("""{"type":"future.\u00E9","agentId":"child"}""")]
76+
[InlineData("""{"TYPE":"user.message","data":{"content":"hello"}}""")]
77+
[InlineData("""{"type":"USER.MESSAGE","data":{"content":"hello"}}""")]
78+
[InlineData("""{"type":"future.event","agentId":"child","ephemeral":true,"data":[null,{"type":"user.message"}]}""")]
79+
[InlineData("""{"agentId":"child","ephemeral":true,"data":{"type":"user.message"}}""")]
80+
[InlineData("""{"type":42,"agentId":"child","ephemeral":true}""")]
81+
[InlineData("""{"type":-2147483648}""")]
82+
[InlineData("""{"type":2147483647}""")]
83+
[InlineData("""{"type":"user.message","DATA":{"CONTENT":"hello"}}""")]
84+
[InlineData("""{"type":"user.message","data":{"content":"hello"},"data":{"content":"last"}}""")]
85+
[InlineData("null")]
86+
public void FromJson_PreservesPolymorphicDeserialization(string json)
87+
{
88+
var expected = JsonSerializer.Deserialize(json, PolymorphicTypeInfo);
89+
var actual = SessionEvent.FromJson(json);
90+
91+
Assert.Equal(expected?.GetType(), actual?.GetType());
92+
Assert.True(JsonNode.DeepEquals(
93+
JsonNode.Parse(expected?.ToJson() ?? "null"),
94+
JsonNode.Parse(actual?.ToJson() ?? "null")));
95+
}
96+
97+
[Theory]
98+
[InlineData("""{"type":null}""")]
99+
[InlineData("""{"type":true}""")]
100+
[InlineData("""{"type":{}}""")]
101+
[InlineData("""{"type":[]}""")]
102+
[InlineData("""{"type":1.5}""")]
103+
[InlineData("""{"type":2147483648}""")]
104+
[InlineData("""{"type":"\uD800"}""")]
105+
[InlineData("""{"type":"session.idle","\uD800":true,"data":{}}""")]
106+
[InlineData("""{"type":"user.message","data":{"content":"hello"}""")]
107+
[InlineData("""{"type":"user.message","data":{"content":"hello"},}""")]
108+
[InlineData("""{"type":"future.event","ty\u0070e":"future.event"}""")]
109+
[InlineData("""{"\u0024future":true,"type":"session.idle","data":{}}""")]
110+
[InlineData("""{"type":"user.message","type":"session.idle","data":{"content":"hello"}}""")]
111+
[InlineData("""{"type":"user.message","data":{"content":"hello"},"type":"user.message"}""")]
112+
[InlineData("""{"type":"future.event","data":{},"type":"future.event"}""")]
113+
[InlineData("""{"type":"user.message","data":{}}""")]
114+
[InlineData("""{"type":"user.message"}""")]
115+
[InlineData("""{"$id":"1","type":"user.message","data":{"content":"hello"}}""")]
116+
[InlineData("""{"type":"user.message","data":{"content":"hello"},"$ref":"1"}""")]
117+
[InlineData("""{"type":"future.event","$values":[]}""")]
118+
[InlineData("""{"$future":"ignored","type":"user.message","data":{"content":"hello"}}""")]
119+
[InlineData("""{"type":"session.idle","id":"invalid","data":{}}""")]
120+
[InlineData("""{"type":"session.idle","timestamp":"invalid","data":{}}""")]
121+
[InlineData("""{"type":"session.idle","ephemeral":"invalid","data":{}}""")]
122+
[InlineData("""[{"type":"session.idle","data":{}}]""")]
123+
[InlineData("42")]
124+
[InlineData("true")]
125+
[InlineData("@")]
126+
[InlineData("")]
127+
[InlineData("\"text\"")]
128+
[InlineData("""{"type":"user.message","data":{"content":"hello"}} trailing""")]
129+
public void FromJson_RejectsInvalidEventsLikePolymorphicDeserialization(string json)
130+
{
131+
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize(json, PolymorphicTypeInfo));
132+
Assert.Throws<JsonException>(() => SessionEvent.FromJson(json));
133+
}
134+
135+
[Theory]
136+
[InlineData(0)]
137+
[InlineData(32)]
138+
[InlineData(256)]
139+
[InlineData(100_000)]
140+
public void FromJson_PreservesUnicodeAcrossBufferSizes(int repetitions)
141+
{
142+
var content = string.Concat(Enumerable.Repeat("a\u00E9\u4E2D\U0001F600", repetitions));
143+
var json = "{\"data\":{\"content\":\"" + content + "\"},\"ty\\u0070e\":\"user.message\"}";
144+
145+
var actual = Assert.IsType<UserMessageEvent>(SessionEvent.FromJson(json));
146+
147+
Assert.Equal(content, actual.Data.Content);
148+
Assert.Equal(JsonSerializer.Deserialize(json, PolymorphicTypeInfo)?.ToJson(), actual.ToJson());
149+
}
150+
151+
[Theory]
152+
[InlineData('\uD800', 0)]
153+
[InlineData('\uDC00', 0)]
154+
[InlineData('\uD800', 1000)]
155+
[InlineData('\uDC00', 1000)]
156+
public void FromJson_RejectsInvalidUtf16LikePolymorphicDeserialization(char invalid, int padding)
157+
{
158+
var json = "{\"type\":\"user.message\",\"data\":{\"content\":\"" + new string('a', padding) + invalid + "\"}}";
159+
160+
Assert.Throws<ArgumentException>(() => JsonSerializer.Deserialize(json, PolymorphicTypeInfo));
161+
Assert.Throws<ArgumentException>(() => SessionEvent.FromJson(json));
162+
}
163+
164+
[Fact]
165+
public void FromJson_RejectsNullInputLikePolymorphicDeserialization()
166+
{
167+
Assert.Throws<ArgumentNullException>(() => JsonSerializer.Deserialize((string)null!, PolymorphicTypeInfo));
168+
Assert.Throws<ArgumentNullException>(() => SessionEvent.FromJson(null!));
169+
}
170+
171+
[Fact]
172+
public async Task FromJson_IsSafeForConcurrentDeserialization()
173+
{
174+
var events = EventTypes.ToArray();
175+
var tasks = Enumerable.Range(0, 32).Select(i => Task.Run(() =>
176+
{
177+
foreach (var item in events)
178+
{
179+
var discriminator = (string)item[0];
180+
var expectedType = (Type)item[1];
181+
var json = $$"""{"type":"{{discriminator}}","agentId":"{{i}}","data":null}""";
182+
var actual = SessionEvent.FromJson(json);
183+
184+
Assert.Equal(expectedType, actual.GetType());
185+
Assert.Equal(i.ToString(), actual.AgentId);
186+
}
187+
}));
188+
189+
await Task.WhenAll(tasks);
190+
}
191+
192+
[Fact]
193+
public void LiveOptions_SerializeEventsWithoutChangingTheWireFormat()
194+
{
195+
var options = (JsonSerializerOptions)typeof(CopilotClient).GetProperty(
196+
"SerializerOptionsForMessageFormatter", BindingFlags.Static | BindingFlags.NonPublic)!
197+
.GetValue(null)!;
198+
var sessionEvent = new UserMessageEvent
199+
{
200+
Id = Guid.Parse("11111111-1111-1111-1111-111111111111"),
201+
Timestamp = DateTimeOffset.Parse("2026-09-30T00:00:00Z"),
202+
Data = new UserMessageData { Content = "hello" },
203+
};
204+
205+
var json = JsonSerializer.Serialize<SessionEvent>(sessionEvent, options);
206+
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(sessionEvent.ToJson()), JsonNode.Parse(json)));
207+
var decoded = Assert.IsType<UserMessageEvent>(JsonSerializer.Deserialize<SessionEvent>(json, options));
208+
Assert.Equal("hello", decoded.Data.Content);
209+
}
210+
211+
// The unchanged source-generated context is the reference contract, not reflection metadata.
212+
private static JsonTypeInfo<SessionEvent> PolymorphicTypeInfo
213+
{
214+
get
215+
{
216+
var contextType = typeof(SessionEvent).Assembly.GetType("GitHub.Copilot.SessionEventsJsonContext", true)!;
217+
var context = (JsonSerializerContext)contextType.GetProperty("Default")!.GetValue(null)!;
218+
return (JsonTypeInfo<SessionEvent>)context.GetTypeInfo(typeof(SessionEvent))!;
219+
}
220+
}
221+
}

‎nodejs/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"url": "https://github.com/github/copilot-sdk.git"
66
},
77
"version": "0.0.0-dev",
8-
"copilotCliVersion": "1.0.92-1",
8+
"copilotCliVersion": "1.0.92-2",
99
"description": "TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC",
1010
"main": "./dist/cjs/index.js",
1111
"types": "./dist/index.d.ts",

0 commit comments

Comments
 (0)