Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,7 @@ private void ApplyConfigDefaultsForMode(SessionConfigBase config)
{
if (_options.Mode == CopilotClientMode.Empty)
{
config.EnableExperimentalMode ??= false;
config.EnableSessionTelemetry ??= false;
config.SkipEmbeddingRetrieval ??= true;
config.EmbeddingCacheStorage ??= EmbeddingCacheStorageMode.InMemory;
Expand Down Expand Up @@ -1146,6 +1147,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
config.Provider,
config.Capi,
config.EnableSessionTelemetry,
config.EnableExperimentalMode,
config.OnPermissionRequest != null ? true : null,
config.OnUserInputRequest != null ? true : null,
config.OnExitPlanModeRequest != null ? true : null,
Expand Down Expand Up @@ -1360,6 +1362,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
config.Provider,
config.Capi,
config.EnableSessionTelemetry,
config.EnableExperimentalMode,
config.OnPermissionRequest != null ? true : null,
config.OnUserInputRequest != null ? true : null,
config.OnExitPlanModeRequest != null ? true : null,
Expand Down Expand Up @@ -2715,6 +2718,7 @@ internal record CreateSessionRequest(
ProviderConfig? Provider,
CapiSessionOptions? Capi,
bool? EnableSessionTelemetry,
bool? IsExperimentalMode,
Comment thread
SteveSandersonMS marked this conversation as resolved.
bool? RequestPermission,
bool? RequestUserInput,
bool? RequestExitPlanMode,
Expand Down Expand Up @@ -2821,6 +2825,7 @@ internal record ResumeSessionRequest(
ProviderConfig? Provider,
CapiSessionOptions? Capi,
bool? EnableSessionTelemetry,
bool? IsExperimentalMode,
bool? RequestPermission,
bool? RequestUserInput,
bool? RequestExitPlanMode,
Expand Down
10 changes: 10 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3068,6 +3068,7 @@ protected SessionConfigBase(SessionConfigBase? other)
Providers = other.Providers is not null ? [.. other.Providers] : null;
Models = other.Models is not null ? [.. other.Models] : null;
EnableSessionTelemetry = other.EnableSessionTelemetry;
EnableExperimentalMode = other.EnableExperimentalMode;
SkipCustomInstructions = other.SkipCustomInstructions;
CustomAgentsLocalOnly = other.CustomAgentsLocalOnly;
CoauthorEnabled = other.CoauthorEnabled;
Expand Down Expand Up @@ -3270,6 +3271,15 @@ protected SessionConfigBase(SessionConfigBase? other)
/// </summary>
public bool? EnableSessionTelemetry { get; set; }

/// <summary>
/// Controls whether the session enables experimental features.
/// </summary>
/// <remarks>
/// Defaults to <see langword="false"/> in <see cref="CopilotClientMode.Empty"/>.
/// Otherwise, the runtime decides when left <see langword="null"/>.
/// </remarks>
public bool? EnableExperimentalMode { get; set; }
Comment thread
SteveSandersonMS marked this conversation as resolved.

/// <summary>
/// When <see langword="true"/>, suppresses loading of custom instruction files
/// (e.g. <c>.github/copilot-instructions.md</c>, <c>AGENTS.md</c>) from the working directory.
Expand Down
35 changes: 35 additions & 0 deletions dotnet/test/Unit/CloneTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
Streaming = true,
EnableCitations = true,
EnableSessionTelemetry = false,
EnableExperimentalMode = true,
EnableOnDemandInstructionDiscovery = true,
IncludeSubAgentStreamingEvents = false,
McpServers = new Dictionary<string, McpServerConfig> { ["server1"] = new McpStdioServerConfig { Command = "echo" } },
Expand Down Expand Up @@ -122,6 +123,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
Assert.Equal(original.Streaming, clone.Streaming);
Assert.Equal(original.EnableCitations, clone.EnableCitations);
Assert.Equal(original.EnableSessionTelemetry, clone.EnableSessionTelemetry);
Assert.Equal(original.EnableExperimentalMode, clone.EnableExperimentalMode);
Assert.Equal(original.EnableOnDemandInstructionDiscovery, clone.EnableOnDemandInstructionDiscovery);
Assert.Equal(original.IncludeSubAgentStreamingEvents, clone.IncludeSubAgentStreamingEvents);
Assert.Equal(original.McpServers.Count, clone.McpServers!.Count);
Expand Down Expand Up @@ -373,6 +375,19 @@ public void ResumeSessionConfig_Clone_CopiesEnableSessionTelemetry()
Assert.False(clone.EnableSessionTelemetry);
}

[Fact]
public void ResumeSessionConfig_Clone_CopiesEnableExperimentalMode()
{
var original = new ResumeSessionConfig
{
EnableExperimentalMode = true,
};

var clone = original.Clone();

Assert.True(clone.EnableExperimentalMode);
}

[Fact]
public void ResumeSessionConfig_Clone_CopiesContinuePendingWork()
{
Expand Down Expand Up @@ -460,6 +475,26 @@ public void ResumeSessionConfig_Clone_PreservesEnableSessionTelemetryDefault()
Assert.Null(clone.EnableSessionTelemetry);
}

[Fact]
public void SessionConfig_Clone_PreservesEnableExperimentalModeDefault()
{
var original = new SessionConfig();

var clone = original.Clone();

Assert.Null(clone.EnableExperimentalMode);
}

[Fact]
public void ResumeSessionConfig_Clone_PreservesEnableExperimentalModeDefault()
{
var original = new ResumeSessionConfig();

var clone = original.Clone();

Assert.Null(clone.EnableExperimentalMode);
}

[Fact]
public void SessionConfig_Clone_CopiesEnableOnDemandInstructionDiscovery()
{
Expand Down
45 changes: 45 additions & 0 deletions dotnet/test/Unit/GitHubTelemetryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,50 @@ await server.SendGitHubTelemetryEventAsync(new Dictionary<string, object?>
Assert.Equal(false, clientInfo.IsStaff);
}

[Fact]
public async Task CreateSession_EmptyMode_Sends_IsExperimentalMode_False_By_Default()
{
await using var server = await FakeTelemetryServer.StartAsync();
await using var client = new CopilotClient(new CopilotClientOptions
{
Connection = RuntimeConnection.ForUri(server.Url),
Mode = CopilotClientMode.Empty,
BaseDirectory = Path.GetTempPath(),
});
await client.StartAsync();

await client.CreateSessionAsync(new SessionConfig
{
AvailableTools = new ToolSet().AddBuiltIn(BuiltInTools.Isolated).ToList(),
});

var createParams = server.LastCreateParams ?? throw new InvalidOperationException("session.create was not captured.");
Assert.True(createParams.TryGetProperty("isExperimentalMode", out var flag));
Assert.False(flag.GetBoolean());
}

[Fact]
public async Task ResumeSession_EmptyMode_Sends_IsExperimentalMode_False_By_Default()
{
await using var server = await FakeTelemetryServer.StartAsync();
await using var client = new CopilotClient(new CopilotClientOptions
{
Connection = RuntimeConnection.ForUri(server.Url),
Mode = CopilotClientMode.Empty,
BaseDirectory = Path.GetTempPath(),
});
await client.StartAsync();

await client.ResumeSessionAsync("session-1", new ResumeSessionConfig
{
AvailableTools = new ToolSet().AddBuiltIn(BuiltInTools.Isolated).ToList(),
});

var resumeParams = server.LastResumeParams ?? throw new InvalidOperationException("session.resume was not captured.");
Assert.True(resumeParams.TryGetProperty("isExperimentalMode", out var flag));
Assert.False(flag.GetBoolean());
}

private sealed class FakeTelemetryServer : IAsyncDisposable
{
private readonly TcpListener _listener;
Expand Down Expand Up @@ -307,6 +351,7 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel
"session.resume" => CaptureResume(request),
"session.send" => new Dictionary<string, object?> { ["messageId"] = "message-1" },
"session.destroy" => new Dictionary<string, object?>(),
"session.options.update" => new Dictionary<string, object?> { ["success"] = true },
"runtime.shutdown" => new Dictionary<string, object?>(),
_ => throw new InvalidOperationException($"Unexpected RPC method '{method}'."),
};
Expand Down
34 changes: 34 additions & 0 deletions dotnet/test/Unit/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,40 @@ public void ResumeSessionRequest_CanSerializeEnableSessionTelemetry_WithSdkOptio
Assert.False(root.GetProperty("enableSessionTelemetry").GetBoolean());
}

[Fact]
public void SessionRequests_CanSerializeEnableExperimentalMode_WithSdkOptions()
{
var options = GetSerializerOptions();

var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest");
var createRequest = CreateInternalRequest(
createRequestType,
("SessionId", "session-id"),
("IsExperimentalMode", false));
var createRoot = JsonDocument.Parse(JsonSerializer.Serialize(createRequest, createRequestType, options)).RootElement;
Assert.False(createRoot.GetProperty("isExperimentalMode").GetBoolean());

var createRequestOmitted = CreateInternalRequest(
createRequestType,
("SessionId", "session-id"));
var createOmittedRoot = JsonDocument.Parse(JsonSerializer.Serialize(createRequestOmitted, createRequestType, options)).RootElement;
Assert.False(createOmittedRoot.TryGetProperty("isExperimentalMode", out _));

var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest");
var resumeRequest = CreateInternalRequest(
resumeRequestType,
("SessionId", "session-id"),
("IsExperimentalMode", true));
var resumeRoot = JsonDocument.Parse(JsonSerializer.Serialize(resumeRequest, resumeRequestType, options)).RootElement;
Assert.True(resumeRoot.GetProperty("isExperimentalMode").GetBoolean());

var resumeRequestOmitted = CreateInternalRequest(
resumeRequestType,
("SessionId", "session-id"));
var resumeOmittedRoot = JsonDocument.Parse(JsonSerializer.Serialize(resumeRequestOmitted, resumeRequestType, options)).RootElement;
Assert.False(resumeOmittedRoot.TryGetProperty("isExperimentalMode", out _));
}

[Fact]
public void CreateSessionRequest_CanSerializeEnableOnDemandInstructionDiscovery_WithSdkOptions()
{
Expand Down
2 changes: 2 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
req.EnableSessionTelemetry = config.EnableSessionTelemetry
req.EnableCitations = config.EnableCitations
req.SessionLimits = config.SessionLimits
req.IsExperimentalMode = config.EnableExperimentalMode
req.SkipCustomInstructions = config.SkipCustomInstructions
req.CustomAgentsLocalOnly = config.CustomAgentsLocalOnly
req.CoauthorEnabled = config.CoauthorEnabled
Expand Down Expand Up @@ -1122,6 +1123,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
req.Providers = config.Providers
req.Models = config.Models
req.EnableSessionTelemetry = config.EnableSessionTelemetry
req.IsExperimentalMode = config.EnableExperimentalMode
req.SkipCustomInstructions = config.SkipCustomInstructions
req.CustomAgentsLocalOnly = config.CustomAgentsLocalOnly
req.CoauthorEnabled = config.CoauthorEnabled
Expand Down
57 changes: 57 additions & 0 deletions go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2333,6 +2333,63 @@ func TestCreateSessionRequest_RequestMCPApps(t *testing.T) {
})
}

func TestSessionRequests_EnableExperimentalMode(t *testing.T) {
t.Run("create forwards enableExperimentalMode when explicitly false", func(t *testing.T) {
req := createSessionRequest{
IsExperimentalMode: Bool(false),
}
data, err := json.Marshal(req)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
if m["isExperimentalMode"] != false {
t.Errorf("Expected isExperimentalMode to be false, got %v", m["isExperimentalMode"])
}
})

t.Run("create omits enableExperimentalMode when unset", func(t *testing.T) {
req := createSessionRequest{}
data, _ := json.Marshal(req)
var m map[string]any
json.Unmarshal(data, &m)
if _, ok := m["isExperimentalMode"]; ok {
t.Error("Expected isExperimentalMode to be omitted when not set")
}
})

t.Run("resume forwards enableExperimentalMode when explicitly true", func(t *testing.T) {
req := resumeSessionRequest{
SessionID: "s1",
IsExperimentalMode: Bool(true),
}
data, err := json.Marshal(req)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
if m["isExperimentalMode"] != true {
t.Errorf("Expected isExperimentalMode to be true, got %v", m["isExperimentalMode"])
}
})

t.Run("resume omits enableExperimentalMode when unset", func(t *testing.T) {
req := resumeSessionRequest{SessionID: "s1"}
data, _ := json.Marshal(req)
var m map[string]any
json.Unmarshal(data, &m)
if _, ok := m["isExperimentalMode"]; ok {
t.Error("Expected isExperimentalMode to be omitted when not set")
}
})
}

func TestResumeSessionRequest_RequestMCPApps(t *testing.T) {
t.Run("sends requestMcpApps flag when EnableMCPApps is set", func(t *testing.T) {
req := resumeSessionRequest{
Expand Down
8 changes: 8 additions & 0 deletions go/mode_empty.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ func (c *Client) applyConfigDefaultsForMode(config *SessionConfig) {
if c.options.Mode != ModeEmpty {
return
}
if config.EnableExperimentalMode == nil {
f := false
config.EnableExperimentalMode = &f
}
if config.EnableSessionTelemetry == nil {
f := false
config.EnableSessionTelemetry = &f
Expand Down Expand Up @@ -170,6 +174,10 @@ func (c *Client) applyResumeDefaultsForMode(config *ResumeSessionConfig) {
if c.options.Mode != ModeEmpty {
return
}
if config.EnableExperimentalMode == nil {
f := false
config.EnableExperimentalMode = &f
}
if config.EnableSessionTelemetry == nil {
f := false
config.EnableSessionTelemetry = &f
Expand Down
36 changes: 36 additions & 0 deletions go/toolset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,24 @@ func TestApplyConfigDefaultsForMode_emptyDefaultsTelemetryFalse(t *testing.T) {
}
}

func TestApplyConfigDefaultsForMode_emptyDefaultsExperimentalModeFalse(t *testing.T) {
c := NewClient(&ClientOptions{Mode: ModeEmpty, BaseDirectory: t.TempDir()})
cfg := &SessionConfig{}
c.applyConfigDefaultsForMode(cfg)
if cfg.EnableExperimentalMode == nil || *cfg.EnableExperimentalMode != false {
t.Errorf("expected experimental mode default false in empty mode, got %v", cfg.EnableExperimentalMode)
}
}

func TestApplyConfigDefaultsForMode_copilotCliLeavesExperimentalModeNil(t *testing.T) {
c := NewClient(&ClientOptions{Mode: ModeCopilotCli})
cfg := &SessionConfig{}
c.applyConfigDefaultsForMode(cfg)
if cfg.EnableExperimentalMode != nil {
t.Errorf("non-empty mode must not default experimental mode")
}
}

func TestApplyConfigDefaultsForMode_emptyHonorsCallerTelemetry(t *testing.T) {
c := NewClient(&ClientOptions{Mode: ModeEmpty, BaseDirectory: t.TempDir()})
trueVal := true
Expand Down Expand Up @@ -398,3 +416,21 @@ func TestApplyConfigDefaultsForMode_copilotCliLeavesMCPOAuthTokenStorageEmpty(t
t.Errorf("non-empty mode must not default MCPOAuthTokenStorage, got %q", cfg.MCPOAuthTokenStorage)
}
}

func TestApplyResumeDefaultsForMode_emptyDefaultsExperimentalModeFalse(t *testing.T) {
c := NewClient(&ClientOptions{Mode: ModeEmpty, BaseDirectory: t.TempDir()})
cfg := &ResumeSessionConfig{}
c.applyResumeDefaultsForMode(cfg)
if cfg.EnableExperimentalMode == nil || *cfg.EnableExperimentalMode != false {
t.Errorf("expected experimental mode default false in empty mode, got %v", cfg.EnableExperimentalMode)
}
}

func TestApplyResumeDefaultsForMode_copilotCliLeavesExperimentalModeNil(t *testing.T) {
c := NewClient(&ClientOptions{Mode: ModeCopilotCli})
cfg := &ResumeSessionConfig{}
c.applyResumeDefaultsForMode(cfg)
if cfg.EnableExperimentalMode != nil {
t.Errorf("non-empty mode must not default experimental mode")
}
}
Loading
Loading