Skip to content

Commit 11dde6e

Browse files
Update to match runtime changes (#737)
* Run codegen to produce session.log and other updates from runtime * E2E test for TypeScript session.log * Session log wrappers and E2E tests for C#, Go, Python * Docs for extension author guide * Avoid incorrect use of console.log as that would break on the stdio transport * Improve extension API via joinSession * Use latest codegenerator after rebase * Fix how we reference vscode-jsonrpc * Update dependency * Formatting
1 parent ad51d74 commit 11dde6e

File tree

24 files changed

+2087
-98
lines changed

24 files changed

+2087
-98
lines changed

dotnet/src/Generated/Rpc.cs

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,28 @@ public class AccountGetQuotaResult
192192
public Dictionary<string, AccountGetQuotaResultQuotaSnapshotsValue> QuotaSnapshots { get; set; } = [];
193193
}
194194

195+
public class SessionLogResult
196+
{
197+
/// <summary>The unique identifier of the emitted session event</summary>
198+
[JsonPropertyName("eventId")]
199+
public Guid EventId { get; set; }
200+
}
201+
202+
internal class SessionLogRequest
203+
{
204+
[JsonPropertyName("sessionId")]
205+
public string SessionId { get; set; } = string.Empty;
206+
207+
[JsonPropertyName("message")]
208+
public string Message { get; set; } = string.Empty;
209+
210+
[JsonPropertyName("level")]
211+
public SessionLogRequestLevel? Level { get; set; }
212+
213+
[JsonPropertyName("ephemeral")]
214+
public bool? Ephemeral { get; set; }
215+
}
216+
195217
public class SessionModelGetCurrentResult
196218
{
197219
[JsonPropertyName("modelId")]
@@ -217,6 +239,9 @@ internal class SessionModelSwitchToRequest
217239

218240
[JsonPropertyName("modelId")]
219241
public string ModelId { get; set; } = string.Empty;
242+
243+
[JsonPropertyName("reasoningEffort")]
244+
public SessionModelSwitchToRequestReasoningEffort? ReasoningEffort { get; set; }
220245
}
221246

222247
public class SessionModeGetResult
@@ -511,6 +536,32 @@ internal class SessionPermissionsHandlePendingPermissionRequestRequest
511536
public object Result { get; set; } = null!;
512537
}
513538

539+
[JsonConverter(typeof(JsonStringEnumConverter<SessionLogRequestLevel>))]
540+
public enum SessionLogRequestLevel
541+
{
542+
[JsonStringEnumMemberName("info")]
543+
Info,
544+
[JsonStringEnumMemberName("warning")]
545+
Warning,
546+
[JsonStringEnumMemberName("error")]
547+
Error,
548+
}
549+
550+
551+
[JsonConverter(typeof(JsonStringEnumConverter<SessionModelSwitchToRequestReasoningEffort>))]
552+
public enum SessionModelSwitchToRequestReasoningEffort
553+
{
554+
[JsonStringEnumMemberName("low")]
555+
Low,
556+
[JsonStringEnumMemberName("medium")]
557+
Medium,
558+
[JsonStringEnumMemberName("high")]
559+
High,
560+
[JsonStringEnumMemberName("xhigh")]
561+
Xhigh,
562+
}
563+
564+
514565
[JsonConverter(typeof(JsonStringEnumConverter<SessionModeGetResultMode>))]
515566
public enum SessionModeGetResultMode
516567
{
@@ -643,6 +694,13 @@ internal SessionRpc(JsonRpc rpc, string sessionId)
643694
public ToolsApi Tools { get; }
644695

645696
public PermissionsApi Permissions { get; }
697+
698+
/// <summary>Calls "session.log".</summary>
699+
public async Task<SessionLogResult> LogAsync(string message, SessionLogRequestLevel? level = null, bool? ephemeral = null, CancellationToken cancellationToken = default)
700+
{
701+
var request = new SessionLogRequest { SessionId = _sessionId, Message = message, Level = level, Ephemeral = ephemeral };
702+
return await CopilotClient.InvokeRpcAsync<SessionLogResult>(_rpc, "session.log", [request], cancellationToken);
703+
}
646704
}
647705

648706
public class ModelApi
@@ -664,9 +722,9 @@ public async Task<SessionModelGetCurrentResult> GetCurrentAsync(CancellationToke
664722
}
665723

666724
/// <summary>Calls "session.model.switchTo".</summary>
667-
public async Task<SessionModelSwitchToResult> SwitchToAsync(string modelId, CancellationToken cancellationToken = default)
725+
public async Task<SessionModelSwitchToResult> SwitchToAsync(string modelId, SessionModelSwitchToRequestReasoningEffort? reasoningEffort = null, CancellationToken cancellationToken = default)
668726
{
669-
var request = new SessionModelSwitchToRequest { SessionId = _sessionId, ModelId = modelId };
727+
var request = new SessionModelSwitchToRequest { SessionId = _sessionId, ModelId = modelId, ReasoningEffort = reasoningEffort };
670728
return await CopilotClient.InvokeRpcAsync<SessionModelSwitchToResult>(_rpc, "session.model.switchTo", [request], cancellationToken);
671729
}
672730
}
@@ -909,6 +967,8 @@ public async Task<SessionPermissionsHandlePendingPermissionRequestResult> Handle
909967
[JsonSerializable(typeof(SessionCompactionCompactResult))]
910968
[JsonSerializable(typeof(SessionFleetStartRequest))]
911969
[JsonSerializable(typeof(SessionFleetStartResult))]
970+
[JsonSerializable(typeof(SessionLogRequest))]
971+
[JsonSerializable(typeof(SessionLogResult))]
912972
[JsonSerializable(typeof(SessionModeGetRequest))]
913973
[JsonSerializable(typeof(SessionModeGetResult))]
914974
[JsonSerializable(typeof(SessionModeSetRequest))]

dotnet/src/Generated/SessionEvents.cs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ namespace GitHub.Copilot.SDK;
6969
[JsonDerivedType(typeof(SubagentSelectedEvent), "subagent.selected")]
7070
[JsonDerivedType(typeof(SubagentStartedEvent), "subagent.started")]
7171
[JsonDerivedType(typeof(SystemMessageEvent), "system.message")]
72+
[JsonDerivedType(typeof(SystemNotificationEvent), "system.notification")]
7273
[JsonDerivedType(typeof(ToolExecutionCompleteEvent), "tool.execution_complete")]
7374
[JsonDerivedType(typeof(ToolExecutionPartialResultEvent), "tool.execution_partial_result")]
7475
[JsonDerivedType(typeof(ToolExecutionProgressEvent), "tool.execution_progress")]
@@ -657,6 +658,18 @@ public partial class SystemMessageEvent : SessionEvent
657658
public required SystemMessageData Data { get; set; }
658659
}
659660

661+
/// <summary>
662+
/// Event: system.notification
663+
/// </summary>
664+
public partial class SystemNotificationEvent : SessionEvent
665+
{
666+
[JsonIgnore]
667+
public override string Type => "system.notification";
668+
669+
[JsonPropertyName("data")]
670+
public required SystemNotificationData Data { get; set; }
671+
}
672+
660673
/// <summary>
661674
/// Event: permission.requested
662675
/// </summary>
@@ -825,6 +838,10 @@ public partial class SessionStartData
825838
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
826839
[JsonPropertyName("context")]
827840
public SessionStartDataContext? Context { get; set; }
841+
842+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
843+
[JsonPropertyName("alreadyInUse")]
844+
public bool? AlreadyInUse { get; set; }
828845
}
829846

830847
public partial class SessionResumeData
@@ -838,6 +855,10 @@ public partial class SessionResumeData
838855
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
839856
[JsonPropertyName("context")]
840857
public SessionResumeDataContext? Context { get; set; }
858+
859+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
860+
[JsonPropertyName("alreadyInUse")]
861+
public bool? AlreadyInUse { get; set; }
841862
}
842863

843864
public partial class SessionErrorData
@@ -1522,6 +1543,15 @@ public partial class SystemMessageData
15221543
public SystemMessageDataMetadata? Metadata { get; set; }
15231544
}
15241545

1546+
public partial class SystemNotificationData
1547+
{
1548+
[JsonPropertyName("content")]
1549+
public required string Content { get; set; }
1550+
1551+
[JsonPropertyName("kind")]
1552+
public required SystemNotificationDataKind Kind { get; set; }
1553+
}
1554+
15251555
public partial class PermissionRequestedData
15261556
{
15271557
[JsonPropertyName("requestId")]
@@ -2095,6 +2125,72 @@ public partial class SystemMessageDataMetadata
20952125
public Dictionary<string, object>? Variables { get; set; }
20962126
}
20972127

2128+
public partial class SystemNotificationDataKindAgentCompleted : SystemNotificationDataKind
2129+
{
2130+
[JsonIgnore]
2131+
public override string Type => "agent_completed";
2132+
2133+
[JsonPropertyName("agentId")]
2134+
public required string AgentId { get; set; }
2135+
2136+
[JsonPropertyName("agentType")]
2137+
public required string AgentType { get; set; }
2138+
2139+
[JsonPropertyName("status")]
2140+
public required SystemNotificationDataKindAgentCompletedStatus Status { get; set; }
2141+
2142+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2143+
[JsonPropertyName("description")]
2144+
public string? Description { get; set; }
2145+
2146+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2147+
[JsonPropertyName("prompt")]
2148+
public string? Prompt { get; set; }
2149+
}
2150+
2151+
public partial class SystemNotificationDataKindShellCompleted : SystemNotificationDataKind
2152+
{
2153+
[JsonIgnore]
2154+
public override string Type => "shell_completed";
2155+
2156+
[JsonPropertyName("shellId")]
2157+
public required string ShellId { get; set; }
2158+
2159+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2160+
[JsonPropertyName("exitCode")]
2161+
public double? ExitCode { get; set; }
2162+
2163+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2164+
[JsonPropertyName("description")]
2165+
public string? Description { get; set; }
2166+
}
2167+
2168+
public partial class SystemNotificationDataKindShellDetachedCompleted : SystemNotificationDataKind
2169+
{
2170+
[JsonIgnore]
2171+
public override string Type => "shell_detached_completed";
2172+
2173+
[JsonPropertyName("shellId")]
2174+
public required string ShellId { get; set; }
2175+
2176+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2177+
[JsonPropertyName("description")]
2178+
public string? Description { get; set; }
2179+
}
2180+
2181+
[JsonPolymorphic(
2182+
TypeDiscriminatorPropertyName = "type",
2183+
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
2184+
[JsonDerivedType(typeof(SystemNotificationDataKindAgentCompleted), "agent_completed")]
2185+
[JsonDerivedType(typeof(SystemNotificationDataKindShellCompleted), "shell_completed")]
2186+
[JsonDerivedType(typeof(SystemNotificationDataKindShellDetachedCompleted), "shell_detached_completed")]
2187+
public partial class SystemNotificationDataKind
2188+
{
2189+
[JsonPropertyName("type")]
2190+
public virtual string Type { get; set; } = string.Empty;
2191+
}
2192+
2193+
20982194
public partial class PermissionRequestShellCommandsItem
20992195
{
21002196
[JsonPropertyName("identifier")]
@@ -2390,6 +2486,15 @@ public enum SystemMessageDataRole
23902486
Developer,
23912487
}
23922488

2489+
[JsonConverter(typeof(JsonStringEnumConverter<SystemNotificationDataKindAgentCompletedStatus>))]
2490+
public enum SystemNotificationDataKindAgentCompletedStatus
2491+
{
2492+
[JsonStringEnumMemberName("completed")]
2493+
Completed,
2494+
[JsonStringEnumMemberName("failed")]
2495+
Failed,
2496+
}
2497+
23932498
[JsonConverter(typeof(JsonStringEnumConverter<PermissionCompletedDataResultKind>))]
23942499
public enum PermissionCompletedDataResultKind
23952500
{
@@ -2536,6 +2641,12 @@ public enum PermissionCompletedDataResultKind
25362641
[JsonSerializable(typeof(SystemMessageData))]
25372642
[JsonSerializable(typeof(SystemMessageDataMetadata))]
25382643
[JsonSerializable(typeof(SystemMessageEvent))]
2644+
[JsonSerializable(typeof(SystemNotificationData))]
2645+
[JsonSerializable(typeof(SystemNotificationDataKind))]
2646+
[JsonSerializable(typeof(SystemNotificationDataKindAgentCompleted))]
2647+
[JsonSerializable(typeof(SystemNotificationDataKindShellCompleted))]
2648+
[JsonSerializable(typeof(SystemNotificationDataKindShellDetachedCompleted))]
2649+
[JsonSerializable(typeof(SystemNotificationEvent))]
25392650
[JsonSerializable(typeof(ToolExecutionCompleteData))]
25402651
[JsonSerializable(typeof(ToolExecutionCompleteDataError))]
25412652
[JsonSerializable(typeof(ToolExecutionCompleteDataResult))]

dotnet/src/Session.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,29 @@ await InvokeRpcAsync<object>(
671671
/// </example>
672672
public async Task SetModelAsync(string model, CancellationToken cancellationToken = default)
673673
{
674-
await Rpc.Model.SwitchToAsync(model, cancellationToken);
674+
await Rpc.Model.SwitchToAsync(model, cancellationToken: cancellationToken);
675+
}
676+
677+
/// <summary>
678+
/// Log a message to the session timeline.
679+
/// The message appears in the session event stream and is visible to SDK consumers
680+
/// and (for non-ephemeral messages) persisted to the session event log on disk.
681+
/// </summary>
682+
/// <param name="message">The message to log.</param>
683+
/// <param name="level">Log level (default: info).</param>
684+
/// <param name="ephemeral">When <c>true</c>, the message is not persisted to disk.</param>
685+
/// <param name="cancellationToken">Optional cancellation token.</param>
686+
/// <example>
687+
/// <code>
688+
/// await session.LogAsync("Build completed successfully");
689+
/// await session.LogAsync("Disk space low", level: SessionLogRequestLevel.Warning);
690+
/// await session.LogAsync("Connection failed", level: SessionLogRequestLevel.Error);
691+
/// await session.LogAsync("Temporary status", ephemeral: true);
692+
/// </code>
693+
/// </example>
694+
public async Task LogAsync(string message, SessionLogRequestLevel? level = null, bool? ephemeral = null, CancellationToken cancellationToken = default)
695+
{
696+
await Rpc.LogAsync(message, level, ephemeral, cancellationToken);
675697
}
676698

677699
/// <summary>

dotnet/test/SessionTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*--------------------------------------------------------------------------------------------*/
44

55
using GitHub.Copilot.SDK.Test.Harness;
6+
using GitHub.Copilot.SDK.Rpc;
67
using Microsoft.Extensions.AI;
78
using System.ComponentModel;
89
using Xunit;
@@ -404,4 +405,51 @@ public async Task Should_Set_Model_On_Existing_Session()
404405
var modelChanged = await modelChangedTask;
405406
Assert.Equal("gpt-4.1", modelChanged.Data.NewModel);
406407
}
408+
409+
[Fact]
410+
public async Task Should_Log_Messages_At_Various_Levels()
411+
{
412+
var session = await CreateSessionAsync();
413+
var events = new List<SessionEvent>();
414+
session.On(evt => events.Add(evt));
415+
416+
await session.LogAsync("Info message");
417+
await session.LogAsync("Warning message", level: SessionLogRequestLevel.Warning);
418+
await session.LogAsync("Error message", level: SessionLogRequestLevel.Error);
419+
await session.LogAsync("Ephemeral message", ephemeral: true);
420+
421+
// Poll until all 4 notification events arrive
422+
await WaitForAsync(() =>
423+
{
424+
var notifications = events.Where(e =>
425+
e is SessionInfoEvent info && info.Data.InfoType == "notification" ||
426+
e is SessionWarningEvent warn && warn.Data.WarningType == "notification" ||
427+
e is SessionErrorEvent err && err.Data.ErrorType == "notification"
428+
).ToList();
429+
return notifications.Count >= 4;
430+
}, timeout: TimeSpan.FromSeconds(10));
431+
432+
var infoEvent = events.OfType<SessionInfoEvent>().First(e => e.Data.Message == "Info message");
433+
Assert.Equal("notification", infoEvent.Data.InfoType);
434+
435+
var warningEvent = events.OfType<SessionWarningEvent>().First(e => e.Data.Message == "Warning message");
436+
Assert.Equal("notification", warningEvent.Data.WarningType);
437+
438+
var errorEvent = events.OfType<SessionErrorEvent>().First(e => e.Data.Message == "Error message");
439+
Assert.Equal("notification", errorEvent.Data.ErrorType);
440+
441+
var ephemeralEvent = events.OfType<SessionInfoEvent>().First(e => e.Data.Message == "Ephemeral message");
442+
Assert.Equal("notification", ephemeralEvent.Data.InfoType);
443+
}
444+
445+
private static async Task WaitForAsync(Func<bool> condition, TimeSpan timeout)
446+
{
447+
var deadline = DateTime.UtcNow + timeout;
448+
while (!condition())
449+
{
450+
if (DateTime.UtcNow > deadline)
451+
throw new TimeoutException($"Condition not met within {timeout}");
452+
await Task.Delay(100);
453+
}
454+
}
407455
}

0 commit comments

Comments
 (0)