From 982ddfc3fd81e2e688995f73d6c899181c05baae Mon Sep 17 00:00:00 2001 From: jmoseley Date: Mon, 8 Jun 2026 10:32:00 -0700 Subject: [PATCH 1/6] rust: add isExperimentalMode to session create/resume wire Add an optional `is_experimental_mode` field to `SessionConfig` and `ResumeSessionConfig` (plus `with_is_experimental_mode` builders) that serializes as camelCase `isExperimentalMode` and is omitted from the `session.create` / `session.resume` wire when `None`. Lets a consumer disable (`false`) or force-enable (`true`) the experimental feature-flag tier for a single session without persisting to the user's shared config. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rust/src/types.rs | 107 ++++++++++++++++++++++++++++++++++++++++++++++ rust/src/wire.rs | 4 ++ 2 files changed, 111 insertions(+) diff --git a/rust/src/types.rs b/rust/src/types.rs index 8b9b5960a..60f61a74d 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -1246,6 +1246,20 @@ pub struct SessionConfig { /// /// Defaults to `None` (treated as `false`). pub enable_mcp_apps: Option, + /// Disable, force-enable, or inherit the experimental feature-flag tier + /// for this session only. + /// + /// - `Some(false)` — the runtime re-resolves this session's feature flags + /// as if experimental mode were off, stripping experimental-tier flags. + /// - `Some(true)` — force-enables the experimental tier even if the CLI + /// process didn't start with it. + /// - `None` (default) — inherits the CLI process's flags unchanged. + /// + /// This never persists anything to the user's shared config; it only + /// affects the feature-flag resolution for this one session. Serializes + /// as `isExperimentalMode` and is omitted from the wire when `None`, so + /// older CLIs that don't understand it are unaffected. + pub is_experimental_mode: Option, /// Skill directory paths passed through to the GitHub Copilot CLI. pub skill_directories: Option>, /// Additional directories to search for custom instruction files. @@ -1418,6 +1432,7 @@ impl std::fmt::Debug for SessionConfig { .field("enable_session_store", &self.enable_session_store) .field("enable_skills", &self.enable_skills) .field("enable_mcp_apps", &self.enable_mcp_apps) + .field("is_experimental_mode", &self.is_experimental_mode) .field("skill_directories", &self.skill_directories) .field("instruction_directories", &self.instruction_directories) .field("plugin_directories", &self.plugin_directories) @@ -1517,6 +1532,7 @@ impl Default for SessionConfig { enable_skills: None, embedding_cache_storage: None, enable_mcp_apps: None, + is_experimental_mode: None, skill_directories: None, instruction_directories: None, plugin_directories: None, @@ -1659,6 +1675,7 @@ impl SessionConfig { request_auto_mode_switch, request_elicitation, request_mcp_apps: self.enable_mcp_apps.unwrap_or(false), + is_experimental_mode: self.is_experimental_mode, hooks: hooks_flag, skill_directories: self.skill_directories, instruction_directories: self.instruction_directories, @@ -2003,6 +2020,14 @@ impl SessionConfig { self } + /// Disable (`false`) or force-enable (`true`) the experimental feature-flag + /// tier for this session only. `None` (default) inherits the CLI process's + /// flags. Never persists to config. See the field docs for resume caveats. + pub fn with_is_experimental_mode(mut self, is_experimental_mode: bool) -> Self { + self.is_experimental_mode = Some(is_experimental_mode); + self + } + /// Set skill directory paths passed through to the CLI. pub fn with_skill_directories(mut self, paths: I) -> Self where @@ -2252,6 +2277,20 @@ pub struct ResumeSessionConfig { /// Enable MCP Apps (SEP-1865) UI passthrough on resume. See /// [`SessionConfig::enable_mcp_apps`]. Defaults to `None` (treated as `false`). pub enable_mcp_apps: Option, + /// Disable, force-enable, or inherit the experimental feature-flag tier + /// for this resumed session only. + /// + /// - `Some(false)` — re-resolves this session's feature flags as if + /// experimental mode were off, stripping experimental-tier flags. + /// - `Some(true)` — force-enables the experimental tier even if the CLI + /// process didn't start with it. + /// - `None` (default) — inherits the CLI process's flags unchanged. + /// + /// Never persists to config. Note: resume only re-resolves flags on the + /// cold-load path (the session is not already live in-process); an + /// already-active session keeps the flags it was created with. Serializes + /// as `isExperimentalMode` and is omitted from the wire when `None`. + pub is_experimental_mode: Option, /// Skill directory paths passed through to the GitHub Copilot CLI on resume. pub skill_directories: Option>, /// Additional directories to search for custom instruction files on @@ -2395,6 +2434,7 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("enable_session_store", &self.enable_session_store) .field("enable_skills", &self.enable_skills) .field("enable_mcp_apps", &self.enable_mcp_apps) + .field("is_experimental_mode", &self.is_experimental_mode) .field("skill_directories", &self.skill_directories) .field("instruction_directories", &self.instruction_directories) .field("plugin_directories", &self.plugin_directories) @@ -2538,6 +2578,7 @@ impl ResumeSessionConfig { request_auto_mode_switch, request_elicitation, request_mcp_apps: self.enable_mcp_apps.unwrap_or(false), + is_experimental_mode: self.is_experimental_mode, hooks: hooks_flag, skill_directories: self.skill_directories, instruction_directories: self.instruction_directories, @@ -2614,6 +2655,7 @@ impl ResumeSessionConfig { enable_skills: None, embedding_cache_storage: None, enable_mcp_apps: None, + is_experimental_mode: None, skill_directories: None, instruction_directories: None, plugin_directories: None, @@ -2933,6 +2975,14 @@ impl ResumeSessionConfig { self } + /// Disable (`false`) or force-enable (`true`) the experimental feature-flag + /// tier for this session only. `None` (default) inherits the CLI process's + /// flags. Never persists to config. See the field docs for resume caveats. + pub fn with_is_experimental_mode(mut self, is_experimental_mode: bool) -> Self { + self.is_experimental_mode = Some(is_experimental_mode); + self + } + /// Set skill directory paths passed through to the CLI on resume. pub fn with_skill_directories(mut self, paths: I) -> Self where @@ -4453,6 +4503,63 @@ mod tests { assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true)); } + #[test] + fn session_config_is_experimental_mode_serializes_when_set() { + let cfg = SessionConfig::default().with_is_experimental_mode(false); + assert_eq!(cfg.is_experimental_mode, Some(false)); + + let (wire, _runtime) = cfg + .into_wire(Some(SessionId::from("experimental-mode"))) + .expect("is_experimental_mode config has no duplicate handlers"); + assert_eq!(wire.is_experimental_mode, Some(false)); + + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false)); + } + + #[test] + fn session_config_is_experimental_mode_omitted_when_none() { + let cfg = SessionConfig::default(); + assert_eq!(cfg.is_experimental_mode, None); + + let (wire, _runtime) = cfg + .into_wire(Some(SessionId::from("no-experimental-mode"))) + .expect("default config has no duplicate handlers"); + assert_eq!(wire.is_experimental_mode, None); + + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("isExperimentalMode").is_none()); + } + + #[test] + fn resume_session_config_is_experimental_mode_serializes_when_set() { + let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode")) + .with_is_experimental_mode(false); + assert_eq!(cfg.is_experimental_mode, Some(false)); + + let (wire, _runtime) = cfg + .into_wire() + .expect("resume is_experimental_mode config has no duplicate handlers"); + assert_eq!(wire.is_experimental_mode, Some(false)); + + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false)); + } + + #[test] + fn resume_session_config_is_experimental_mode_omitted_when_none() { + let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode")); + assert_eq!(cfg.is_experimental_mode, None); + + let (wire, _runtime) = cfg + .into_wire() + .expect("default resume config has no duplicate handlers"); + assert_eq!(wire.is_experimental_mode, None); + + let json = serde_json::to_value(&wire).unwrap(); + assert!(json.get("isExperimentalMode").is_none()); + } + #[test] #[allow(clippy::field_reassign_with_default)] fn session_config_into_wire_serializes_bucket_b_fields() { diff --git a/rust/src/wire.rs b/rust/src/wire.rs index de40720b2..cf61091e1 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -147,6 +147,8 @@ pub(crate) struct SessionCreateWire { pub include_sub_agent_streaming_events: Option, #[serde(skip_serializing_if = "Option::is_none")] pub commands: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub is_experimental_mode: Option, } /// The exact JSON shape sent on the `session.resume` JSON-RPC request. @@ -257,4 +259,6 @@ pub(crate) struct SessionResumeWire { pub suppress_resume_event: Option, #[serde(skip_serializing_if = "Option::is_none")] pub continue_pending_work: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub is_experimental_mode: Option, } From d2b884818701f10e4cc7f51dcf35bce1a5c34a01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:34:10 +0000 Subject: [PATCH 2/6] Add isExperimentalMode support across remaining SDKs Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com> --- dotnet/src/Client.cs | 4 ++ dotnet/src/Types.cs | 12 ++++ dotnet/test/Unit/CloneTests.cs | 35 ++++++++++++ dotnet/test/Unit/SerializationTests.cs | 34 +++++++++++ go/client.go | 2 + go/client_test.go | 57 +++++++++++++++++++ go/types.go | 14 +++++ .../github/copilot/SessionRequestBuilder.java | 2 + .../copilot/rpc/CreateSessionRequest.java | 19 +++++++ .../copilot/rpc/ResumeSessionConfig.java | 39 +++++++++++++ .../copilot/rpc/ResumeSessionRequest.java | 19 +++++++ .../com/github/copilot/rpc/SessionConfig.java | 39 +++++++++++++ .../copilot/SessionRequestBuilderTest.java | 27 +++++++++ nodejs/src/client.ts | 2 + nodejs/src/types.ts | 9 +++ nodejs/test/client.test.ts | 32 +++++++++++ python/copilot/client.py | 16 ++++++ python/test_client.py | 33 +++++++++++ 18 files changed, 395 insertions(+) diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 4e8715bd5..139610127 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -896,6 +896,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance toolFilter.ExcludedTools, config.Provider, config.EnableSessionTelemetry, + config.IsExperimentalMode, config.OnPermissionRequest != null ? true : null, config.OnUserInputRequest != null ? true : null, config.OnExitPlanModeRequest != null ? true : null, @@ -1091,6 +1092,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes toolFilter.ExcludedTools, config.Provider, config.EnableSessionTelemetry, + config.IsExperimentalMode, config.OnPermissionRequest != null ? true : null, config.OnUserInputRequest != null ? true : null, config.OnExitPlanModeRequest != null ? true : null, @@ -2283,6 +2285,7 @@ internal record CreateSessionRequest( IList? ExcludedTools, ProviderConfig? Provider, bool? EnableSessionTelemetry, + bool? IsExperimentalMode, bool? RequestPermission, bool? RequestUserInput, bool? RequestExitPlanMode, @@ -2369,6 +2372,7 @@ internal record ResumeSessionRequest( IList? ExcludedTools, ProviderConfig? Provider, bool? EnableSessionTelemetry, + bool? IsExperimentalMode, bool? RequestPermission, bool? RequestUserInput, bool? RequestExitPlanMode, diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 7a2ad2951..92e72b5dc 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2474,6 +2474,7 @@ protected SessionConfigBase(SessionConfigBase? other) OnUserInputRequest = other.OnUserInputRequest; Provider = other.Provider; EnableSessionTelemetry = other.EnableSessionTelemetry; + IsExperimentalMode = other.IsExperimentalMode; SkipCustomInstructions = other.SkipCustomInstructions; CustomAgentsLocalOnly = other.CustomAgentsLocalOnly; CoauthorEnabled = other.CoauthorEnabled; @@ -2639,6 +2640,17 @@ protected SessionConfigBase(SessionConfigBase? other) /// public bool? EnableSessionTelemetry { get; set; } + /// + /// Overrides the session's experimental feature-flag tier resolution. + /// + /// + /// Set to to force-enable the experimental tier for this + /// session, or to resolve feature flags as if + /// experimental were off. Leave to inherit the runtime + /// process defaults unchanged. + /// + public bool? IsExperimentalMode { get; set; } + /// /// When , suppresses loading of custom instruction files /// (e.g. .github/copilot-instructions.md, AGENTS.md) from the working directory. diff --git a/dotnet/test/Unit/CloneTests.cs b/dotnet/test/Unit/CloneTests.cs index f64057824..24457e9cc 100644 --- a/dotnet/test/Unit/CloneTests.cs +++ b/dotnet/test/Unit/CloneTests.cs @@ -76,6 +76,7 @@ public void SessionConfig_Clone_CopiesAllProperties() WorkingDirectory = "/workspace", Streaming = true, EnableSessionTelemetry = false, + IsExperimentalMode = true, EnableOnDemandInstructionDiscovery = true, IncludeSubAgentStreamingEvents = false, McpServers = new Dictionary { ["server1"] = new McpStdioServerConfig { Command = "echo" } }, @@ -115,6 +116,7 @@ public void SessionConfig_Clone_CopiesAllProperties() Assert.Equal(original.WorkingDirectory, clone.WorkingDirectory); Assert.Equal(original.Streaming, clone.Streaming); Assert.Equal(original.EnableSessionTelemetry, clone.EnableSessionTelemetry); + Assert.Equal(original.IsExperimentalMode, clone.IsExperimentalMode); Assert.Equal(original.EnableOnDemandInstructionDiscovery, clone.EnableOnDemandInstructionDiscovery); Assert.Equal(original.IncludeSubAgentStreamingEvents, clone.IncludeSubAgentStreamingEvents); Assert.Equal(original.McpServers.Count, clone.McpServers!.Count); @@ -355,6 +357,19 @@ public void ResumeSessionConfig_Clone_CopiesEnableSessionTelemetry() Assert.False(clone.EnableSessionTelemetry); } + [Fact] + public void ResumeSessionConfig_Clone_CopiesIsExperimentalMode() + { + var original = new ResumeSessionConfig + { + IsExperimentalMode = true, + }; + + var clone = original.Clone(); + + Assert.True(clone.IsExperimentalMode); + } + [Fact] public void ResumeSessionConfig_Clone_CopiesContinuePendingWork() { @@ -440,6 +455,26 @@ public void ResumeSessionConfig_Clone_PreservesEnableSessionTelemetryDefault() Assert.Null(clone.EnableSessionTelemetry); } + [Fact] + public void SessionConfig_Clone_PreservesIsExperimentalModeDefault() + { + var original = new SessionConfig(); + + var clone = original.Clone(); + + Assert.Null(clone.IsExperimentalMode); + } + + [Fact] + public void ResumeSessionConfig_Clone_PreservesIsExperimentalModeDefault() + { + var original = new ResumeSessionConfig(); + + var clone = original.Clone(); + + Assert.Null(clone.IsExperimentalMode); + } + [Fact] public void SessionConfig_Clone_CopiesEnableOnDemandInstructionDiscovery() { diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index b0797d34b..2d47e783a 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -299,6 +299,40 @@ public void ResumeSessionRequest_CanSerializeEnableSessionTelemetry_WithSdkOptio Assert.False(root.GetProperty("enableSessionTelemetry").GetBoolean()); } + [Fact] + public void SessionRequests_CanSerializeIsExperimentalMode_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() { diff --git a/go/client.go b/go/client.go index cad460557..07deacf8c 100644 --- a/go/client.go +++ b/go/client.go @@ -629,6 +629,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.ToolFilterPrecedence = precedence req.Provider = config.Provider req.EnableSessionTelemetry = config.EnableSessionTelemetry + req.IsExperimentalMode = config.IsExperimentalMode req.SkipCustomInstructions = config.SkipCustomInstructions req.CustomAgentsLocalOnly = config.CustomAgentsLocalOnly req.CoauthorEnabled = config.CoauthorEnabled @@ -923,6 +924,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.Tools = config.Tools req.Provider = config.Provider req.EnableSessionTelemetry = config.EnableSessionTelemetry + req.IsExperimentalMode = config.IsExperimentalMode req.SkipCustomInstructions = config.SkipCustomInstructions req.CustomAgentsLocalOnly = config.CustomAgentsLocalOnly req.CoauthorEnabled = config.CoauthorEnabled diff --git a/go/client_test.go b/go/client_test.go index d5ba47da8..2e2987a8a 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -1242,6 +1242,63 @@ func TestCreateSessionRequest_RequestMCPApps(t *testing.T) { }) } +func TestSessionRequests_IsExperimentalMode(t *testing.T) { + t.Run("create forwards isExperimentalMode 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 isExperimentalMode 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 isExperimentalMode 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 isExperimentalMode 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{ diff --git a/go/types.go b/go/types.go index 7ffd454a3..88ce9aaeb 100644 --- a/go/types.go +++ b/go/types.go @@ -980,6 +980,12 @@ type SessionConfig struct { // regardless of this setting. This is independent of the OpenTelemetry // configuration in ClientOptions.Telemetry. EnableSessionTelemetry *bool + // IsExperimentalMode, when non-nil, overrides the session's experimental + // feature-flag tier resolution. Use Bool(true) to force-enable the + // experimental tier for this session, Bool(false) to resolve feature flags + // as if experimental were off, or nil to inherit the runtime process + // defaults unchanged. + IsExperimentalMode *bool // SkipCustomInstructions, when non-nil, controls whether the runtime loads // custom instruction files. See also [ClientOptions.Mode] = [ModeEmpty]. SkipCustomInstructions *bool @@ -1295,6 +1301,12 @@ type ResumeSessionConfig struct { // regardless of this setting. This is independent of the OpenTelemetry // configuration in ClientOptions.Telemetry. EnableSessionTelemetry *bool + // IsExperimentalMode, when non-nil, overrides the resumed session's + // experimental feature-flag tier resolution. Use Bool(true) to force-enable + // the experimental tier for this session, Bool(false) to resolve feature + // flags as if experimental were off, or nil to inherit the runtime process + // defaults unchanged. + IsExperimentalMode *bool // SkipCustomInstructions, when non-nil, controls whether the runtime loads // custom instruction files. See also [ClientOptions.Mode] = [ModeEmpty]. SkipCustomInstructions *bool @@ -1690,6 +1702,7 @@ type createSessionRequest struct { ToolFilterPrecedence *rpc.OptionsUpdateToolFilterPrecedence `json:"toolFilterPrecedence,omitempty"` Provider *ProviderConfig `json:"provider,omitempty"` EnableSessionTelemetry *bool `json:"enableSessionTelemetry,omitempty"` + IsExperimentalMode *bool `json:"isExperimentalMode,omitempty"` SkipCustomInstructions *bool `json:"skipCustomInstructions,omitempty"` CustomAgentsLocalOnly *bool `json:"customAgentsLocalOnly,omitempty"` CoauthorEnabled *bool `json:"coauthorEnabled,omitempty"` @@ -1768,6 +1781,7 @@ type resumeSessionRequest struct { ToolFilterPrecedence *rpc.OptionsUpdateToolFilterPrecedence `json:"toolFilterPrecedence,omitempty"` Provider *ProviderConfig `json:"provider,omitempty"` EnableSessionTelemetry *bool `json:"enableSessionTelemetry,omitempty"` + IsExperimentalMode *bool `json:"isExperimentalMode,omitempty"` SkipCustomInstructions *bool `json:"skipCustomInstructions,omitempty"` CustomAgentsLocalOnly *bool `json:"customAgentsLocalOnly,omitempty"` CoauthorEnabled *bool `json:"coauthorEnabled,omitempty"` diff --git a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java index ded92a506..77274a844 100644 --- a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -114,6 +114,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setExcludedTools(config.getExcludedTools()); request.setProvider(config.getProvider()); config.getEnableSessionTelemetry().ifPresent(request::setEnableSessionTelemetry); + config.getIsExperimentalMode().ifPresent(request::setIsExperimentalMode); if (config.getOnUserInputRequest() != null) { request.setRequestUserInput(true); } @@ -225,6 +226,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setExcludedTools(config.getExcludedTools()); request.setProvider(config.getProvider()); config.getEnableSessionTelemetry().ifPresent(request::setEnableSessionTelemetry); + config.getIsExperimentalMode().ifPresent(request::setIsExperimentalMode); if (config.getOnUserInputRequest() != null) { request.setRequestUserInput(true); } diff --git a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index f7d2d44c3..f563e3f19 100644 --- a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -164,6 +164,10 @@ public final class CreateSessionRequest { @JsonProperty("requestMcpApps") private Boolean requestMcpApps; + @JsonProperty("isExperimentalMode") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean isExperimentalMode; + @JsonProperty("requestExitPlanMode") private Boolean requestExitPlanMode; @@ -759,6 +763,21 @@ public void clearRequestMcpApps() { this.requestMcpApps = null; } + /** Gets the isExperimentalMode flag. @return the flag */ + public Boolean getIsExperimentalMode() { + return isExperimentalMode; + } + + /** Sets the isExperimentalMode flag. @param isExperimentalMode the flag */ + public void setIsExperimentalMode(boolean isExperimentalMode) { + this.isExperimentalMode = isExperimentalMode; + } + + /** Clears the isExperimentalMode setting, reverting to the default behavior. */ + public void clearIsExperimentalMode() { + this.isExperimentalMode = null; + } + /** Gets the requestExitPlanMode flag. @return the flag */ public Boolean getRequestExitPlanMode() { return requestExitPlanMode; diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index aee27e1b1..b60cedc40 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -46,6 +46,7 @@ public class ResumeSessionConfig { private List excludedTools; private ProviderConfig provider; private Boolean enableSessionTelemetry; + private Boolean isExperimentalMode; private Boolean skipCustomInstructions; private Boolean customAgentsLocalOnly; private Boolean coauthorEnabled; @@ -297,6 +298,43 @@ public ResumeSessionConfig clearEnableSessionTelemetry() { return this; } + /** + * Overrides the resumed session's experimental feature-flag tier resolution. + * + * @return {@code true} to force-enable the experimental tier, {@code false} to + * resolve feature flags as if experimental were off, or empty to + * inherit the runtime process defaults unchanged + */ + @JsonIgnore + public Optional getIsExperimentalMode() { + return Optional.ofNullable(isExperimentalMode); + } + + /** + * Overrides the resumed session's experimental feature-flag tier resolution. + * + * @param isExperimentalMode + * {@code true} to force-enable the experimental tier for this + * session, {@code false} to resolve feature flags as if experimental + * were off + * @return this config for method chaining + */ + public ResumeSessionConfig setIsExperimentalMode(boolean isExperimentalMode) { + this.isExperimentalMode = isExperimentalMode; + return this; + } + + /** + * Clears the isExperimentalMode setting, reverting to the runtime default + * behavior. + * + * @return this instance for method chaining + */ + public ResumeSessionConfig clearIsExperimentalMode() { + this.isExperimentalMode = null; + return this; + } + /** * Gets whether custom instruction file loading is suppressed. * @@ -1527,6 +1565,7 @@ public ResumeSessionConfig clone() { copy.excludedTools = this.excludedTools != null ? new ArrayList<>(this.excludedTools) : null; copy.provider = this.provider; copy.enableSessionTelemetry = this.enableSessionTelemetry; + copy.isExperimentalMode = this.isExperimentalMode; copy.reasoningEffort = this.reasoningEffort; copy.reasoningSummary = this.reasoningSummary; copy.contextTier = this.contextTier; diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index c8cdc5a2d..605563528 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -169,6 +169,10 @@ public final class ResumeSessionRequest { @JsonProperty("requestMcpApps") private Boolean requestMcpApps; + @JsonProperty("isExperimentalMode") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean isExperimentalMode; + @JsonProperty("requestExitPlanMode") private Boolean requestExitPlanMode; @@ -784,6 +788,21 @@ public void clearRequestMcpApps() { this.requestMcpApps = null; } + /** Gets the isExperimentalMode flag. @return the flag */ + public Boolean getIsExperimentalMode() { + return isExperimentalMode; + } + + /** Sets the isExperimentalMode flag. @param isExperimentalMode the flag */ + public void setIsExperimentalMode(boolean isExperimentalMode) { + this.isExperimentalMode = isExperimentalMode; + } + + /** Clears the isExperimentalMode setting, reverting to the default behavior. */ + public void clearIsExperimentalMode() { + this.isExperimentalMode = null; + } + /** Gets the requestExitPlanMode flag. @return the flag */ public Boolean getRequestExitPlanMode() { return requestExitPlanMode; diff --git a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java index fa7fd2244..e681001dd 100644 --- a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -50,6 +50,7 @@ public class SessionConfig { private List excludedTools; private ProviderConfig provider; private Boolean enableSessionTelemetry; + private Boolean isExperimentalMode; private Boolean skipCustomInstructions; private Boolean customAgentsLocalOnly; private Boolean coauthorEnabled; @@ -398,6 +399,43 @@ public SessionConfig clearEnableSessionTelemetry() { return this; } + /** + * Overrides the session's experimental feature-flag tier resolution. + * + * @return {@code true} to force-enable the experimental tier, {@code false} to + * resolve feature flags as if experimental were off, or empty to + * inherit the runtime process defaults unchanged + */ + @JsonIgnore + public Optional getIsExperimentalMode() { + return Optional.ofNullable(isExperimentalMode); + } + + /** + * Overrides the session's experimental feature-flag tier resolution. + * + * @param isExperimentalMode + * {@code true} to force-enable the experimental tier for this + * session, {@code false} to resolve feature flags as if experimental + * were off + * @return this config instance for method chaining + */ + public SessionConfig setIsExperimentalMode(boolean isExperimentalMode) { + this.isExperimentalMode = isExperimentalMode; + return this; + } + + /** + * Clears the isExperimentalMode setting, reverting to the runtime default + * behavior. + * + * @return this instance for method chaining + */ + public SessionConfig clearIsExperimentalMode() { + this.isExperimentalMode = null; + return this; + } + /** * Gets whether custom instruction file loading is suppressed. * @@ -1650,6 +1688,7 @@ public SessionConfig clone() { copy.excludedTools = this.excludedTools != null ? new ArrayList<>(this.excludedTools) : null; copy.provider = this.provider; copy.enableSessionTelemetry = this.enableSessionTelemetry; + copy.isExperimentalMode = this.isExperimentalMode; copy.skipCustomInstructions = this.skipCustomInstructions; copy.customAgentsLocalOnly = this.customAgentsLocalOnly; copy.coauthorEnabled = this.coauthorEnabled; diff --git a/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java index 9b7e28e85..6b0604ac5 100644 --- a/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java +++ b/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java @@ -98,6 +98,19 @@ void testBuildCreateRequestSetsReasoningSummary() { assertEquals("concise", request.getReasoningSummary()); } + @Test + void testBuildCreateRequestSetsIsExperimentalMode() { + var config = new SessionConfig().setIsExperimentalMode(false); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + assertFalse(request.getIsExperimentalMode()); + } + + @Test + void testBuildCreateRequestOmitsIsExperimentalModeWhenNotSet() { + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(new SessionConfig()); + assertNull(request.getIsExperimentalMode()); + } + @Test void testBuildCreateRequestSetsContextTier() { var config = new SessionConfig().setContextTier("long_context"); @@ -176,6 +189,20 @@ void testBuildResumeRequestOmitsEnableSessionTelemetryWhenNotSet() { assertNull(request.getEnableSessionTelemetry()); } + @Test + void testBuildResumeRequestSetsIsExperimentalMode() { + var config = new ResumeSessionConfig().setIsExperimentalMode(true); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); + assertTrue(request.getIsExperimentalMode()); + } + + @Test + void testBuildResumeRequestOmitsIsExperimentalModeWhenNotSet() { + var config = new ResumeSessionConfig(); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); + assertNull(request.getIsExperimentalMode()); + } + @Test void testBuildResumeRequestWithTools() { var tool = ToolDefinition.create("my_tool", "A tool", Map.of("type", "object"), diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 8dc35b8d7..bd697f06f 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -1100,6 +1100,7 @@ export class CopilotClient { clientName: config.clientName, reasoningEffort: config.reasoningEffort, reasoningSummary: config.reasoningSummary, + isExperimentalMode: config.isExperimentalMode, contextTier: config.contextTier, tools: config.tools?.map((tool) => ({ name: tool.name, @@ -1281,6 +1282,7 @@ export class CopilotClient { model: config.model, reasoningEffort: config.reasoningEffort, reasoningSummary: config.reasoningSummary, + isExperimentalMode: config.isExperimentalMode, contextTier: config.contextTier, systemMessage: wireSystemMessage, availableTools: toolFilterOptions.availableTools, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 75aa5159f..d255e45b2 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1586,6 +1586,15 @@ export interface SessionConfigBase { */ reasoningSummary?: ReasoningSummary; + /** + * Per-session experimental feature-flag tier override. + * + * Set to `true` to force-enable the experimental tier for this session, or + * `false` to resolve feature flags as if experimental were off. Leave + * unset to inherit the runtime process defaults unchanged. + */ + isExperimentalMode?: boolean; + /** * Context window tier for models that support it. Use "long_context" to pin * the session to the long-context tier; omit or use "default" otherwise. diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 657ec7c9c..6a1e2ce23 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -138,6 +138,38 @@ describe("CopilotClient", () => { expect(resumePayload.reasoningSummary).toBe("none"); }); + it("forwards isExperimentalMode in session.create and session.resume", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + + const session = await client.createSession({ + onPermissionRequest: approveAll, + isExperimentalMode: false, + }); + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + isExperimentalMode: true, + }); + + const createPayload = spy.mock.calls.find( + ([method]) => method === "session.create" + )![1] as any; + const resumePayload = spy.mock.calls.find( + ([method]) => method === "session.resume" + )![1] as any; + expect(createPayload.isExperimentalMode).toBe(false); + expect(resumePayload.isExperimentalMode).toBe(true); + }); + it("forwards contextTier in session.create and session.resume", async () => { const client = new CopilotClient(); await client.start(); diff --git a/python/copilot/client.py b/python/copilot/client.py index 7dcec6e8f..831e7164f 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -1563,6 +1563,7 @@ async def create_session( client_name: str | None = None, reasoning_effort: ReasoningEffort | None = None, reasoning_summary: ReasoningSummary | None = None, + is_experimental_mode: bool | None = None, context_tier: ContextTier | None = None, tools: list[Tool] | None = None, system_message: SystemMessageConfig | None = None, @@ -1636,6 +1637,11 @@ async def create_session( reasoning_summary: Reasoning summary mode for supported models. Use ``"none"`` to suppress summary output regardless of whether reasoning is enabled. + is_experimental_mode: Per-session experimental feature-flag tier + override. Set ``True`` to force-enable the experimental tier for + this session or ``False`` to resolve feature flags as if + experimental were off. Omit to inherit the runtime process + defaults unchanged. context_tier: Context window tier for models that support it. Use ``"long_context"`` to pin the session to the long-context tier. tools: Custom tools to register with the session. @@ -1784,6 +1790,8 @@ async def create_session( payload["reasoningEffort"] = reasoning_effort if reasoning_summary: payload["reasoningSummary"] = reasoning_summary + if is_experimental_mode is not None: + payload["isExperimentalMode"] = is_experimental_mode if context_tier: payload["contextTier"] = context_tier if tool_defs: @@ -2174,6 +2182,7 @@ async def resume_session( commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, enable_mcp_apps: bool = False, + is_experimental_mode: bool | None = None, on_exit_plan_mode_request: ExitPlanModeHandler | None = None, on_auto_mode_switch_request: AutoModeSwitchHandler | None = None, create_session_fs_handler: CreateSessionFsHandler | None = None, @@ -2206,6 +2215,11 @@ async def resume_session( reasoning_summary: Reasoning summary mode for supported models. Use ``"none"`` to suppress summary output regardless of whether reasoning is enabled. + is_experimental_mode: Per-session experimental feature-flag tier + override. Set ``True`` to force-enable the experimental tier for + this resumed session or ``False`` to resolve feature flags as if + experimental were off. Omit to inherit the runtime process + defaults unchanged. context_tier: Context window tier for models that support it. Use ``"long_context"`` to pin the session to the long-context tier. tools: Custom tools to register with the session. @@ -2355,6 +2369,8 @@ async def resume_session( payload["reasoningEffort"] = reasoning_effort if reasoning_summary: payload["reasoningSummary"] = reasoning_summary + if is_experimental_mode is not None: + payload["isExperimentalMode"] = is_experimental_mode if context_tier: payload["contextTier"] = context_tier if tool_defs: diff --git a/python/test_client.py b/python/test_client.py index 502d410ab..1e2438e48 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -137,6 +137,39 @@ async def mock_request(method, params, **kwargs): finally: await client.force_stop() + @pytest.mark.asyncio + async def test_create_and_resume_session_forward_is_experimental_mode(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured = {} + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method in ("session.create", "session.resume"): + result = {"sessionId": params.get("sessionId") or "session-1"} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + return {} + + client._client.request = mock_request + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + is_experimental_mode=False, + ) + await client.resume_session( + session.session_id, + on_permission_request=PermissionHandler.approve_all, + is_experimental_mode=True, + ) + + assert captured["session.create"]["isExperimentalMode"] is False + assert captured["session.resume"]["isExperimentalMode"] is True + finally: + await client.force_stop() + @pytest.mark.asyncio async def test_create_and_resume_session_forward_context_tier(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) From c70d1daf997730afaf7a89a6bccfe5e2f28ea659 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:37:51 +0000 Subject: [PATCH 3/6] Fix Java request Javadocs for experimental mode Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com> --- .../github/copilot/rpc/CreateSessionRequest.java | 13 +++++++++++-- .../github/copilot/rpc/ResumeSessionRequest.java | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index f563e3f19..ddf72c6c9 100644 --- a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -763,12 +763,21 @@ public void clearRequestMcpApps() { this.requestMcpApps = null; } - /** Gets the isExperimentalMode flag. @return the flag */ + /** + * Gets the isExperimentalMode flag. + * + * @return the flag + */ public Boolean getIsExperimentalMode() { return isExperimentalMode; } - /** Sets the isExperimentalMode flag. @param isExperimentalMode the flag */ + /** + * Sets the isExperimentalMode flag. + * + * @param isExperimentalMode + * the flag + */ public void setIsExperimentalMode(boolean isExperimentalMode) { this.isExperimentalMode = isExperimentalMode; } diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index 605563528..32574b60f 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -788,12 +788,21 @@ public void clearRequestMcpApps() { this.requestMcpApps = null; } - /** Gets the isExperimentalMode flag. @return the flag */ + /** + * Gets the isExperimentalMode flag. + * + * @return the flag + */ public Boolean getIsExperimentalMode() { return isExperimentalMode; } - /** Sets the isExperimentalMode flag. @param isExperimentalMode the flag */ + /** + * Sets the isExperimentalMode flag. + * + * @param isExperimentalMode + * the flag + */ public void setIsExperimentalMode(boolean isExperimentalMode) { this.isExperimentalMode = isExperimentalMode; } From 1e90676fa7905a15a17124af6a1d615aaeda0e43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:02:47 +0000 Subject: [PATCH 4/6] Rename experimental mode session option across SDKs Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com> --- dotnet/src/Client.cs | 4 +- dotnet/src/Types.cs | 4 +- dotnet/test/Unit/CloneTests.cs | 18 +++--- dotnet/test/Unit/SerializationTests.cs | 2 +- go/client.go | 4 +- go/client_test.go | 10 ++-- go/types.go | 8 +-- .../github/copilot/SessionRequestBuilder.java | 4 +- .../copilot/rpc/ResumeSessionConfig.java | 20 +++---- .../com/github/copilot/rpc/SessionConfig.java | 20 +++---- .../copilot/SessionRequestBuilderTest.java | 12 ++-- nodejs/src/client.ts | 4 +- nodejs/src/types.ts | 2 +- nodejs/test/client.test.ts | 6 +- python/copilot/client.py | 16 +++--- python/test_client.py | 6 +- rust/src/types.rs | 56 +++++++++---------- 17 files changed, 96 insertions(+), 100 deletions(-) diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 139610127..b00cd52c0 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -896,7 +896,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance toolFilter.ExcludedTools, config.Provider, config.EnableSessionTelemetry, - config.IsExperimentalMode, + config.EnableExperimentalMode, config.OnPermissionRequest != null ? true : null, config.OnUserInputRequest != null ? true : null, config.OnExitPlanModeRequest != null ? true : null, @@ -1092,7 +1092,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes toolFilter.ExcludedTools, config.Provider, config.EnableSessionTelemetry, - config.IsExperimentalMode, + config.EnableExperimentalMode, config.OnPermissionRequest != null ? true : null, config.OnUserInputRequest != null ? true : null, config.OnExitPlanModeRequest != null ? true : null, diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 92e72b5dc..28c9cafd3 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2474,7 +2474,7 @@ protected SessionConfigBase(SessionConfigBase? other) OnUserInputRequest = other.OnUserInputRequest; Provider = other.Provider; EnableSessionTelemetry = other.EnableSessionTelemetry; - IsExperimentalMode = other.IsExperimentalMode; + EnableExperimentalMode = other.EnableExperimentalMode; SkipCustomInstructions = other.SkipCustomInstructions; CustomAgentsLocalOnly = other.CustomAgentsLocalOnly; CoauthorEnabled = other.CoauthorEnabled; @@ -2649,7 +2649,7 @@ protected SessionConfigBase(SessionConfigBase? other) /// experimental were off. Leave to inherit the runtime /// process defaults unchanged. /// - public bool? IsExperimentalMode { get; set; } + public bool? EnableExperimentalMode { get; set; } /// /// When , suppresses loading of custom instruction files diff --git a/dotnet/test/Unit/CloneTests.cs b/dotnet/test/Unit/CloneTests.cs index 24457e9cc..ad4fbd988 100644 --- a/dotnet/test/Unit/CloneTests.cs +++ b/dotnet/test/Unit/CloneTests.cs @@ -76,7 +76,7 @@ public void SessionConfig_Clone_CopiesAllProperties() WorkingDirectory = "/workspace", Streaming = true, EnableSessionTelemetry = false, - IsExperimentalMode = true, + EnableExperimentalMode = true, EnableOnDemandInstructionDiscovery = true, IncludeSubAgentStreamingEvents = false, McpServers = new Dictionary { ["server1"] = new McpStdioServerConfig { Command = "echo" } }, @@ -116,7 +116,7 @@ public void SessionConfig_Clone_CopiesAllProperties() Assert.Equal(original.WorkingDirectory, clone.WorkingDirectory); Assert.Equal(original.Streaming, clone.Streaming); Assert.Equal(original.EnableSessionTelemetry, clone.EnableSessionTelemetry); - Assert.Equal(original.IsExperimentalMode, clone.IsExperimentalMode); + 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); @@ -358,16 +358,16 @@ public void ResumeSessionConfig_Clone_CopiesEnableSessionTelemetry() } [Fact] - public void ResumeSessionConfig_Clone_CopiesIsExperimentalMode() + public void ResumeSessionConfig_Clone_CopiesEnableExperimentalMode() { var original = new ResumeSessionConfig { - IsExperimentalMode = true, + EnableExperimentalMode = true, }; var clone = original.Clone(); - Assert.True(clone.IsExperimentalMode); + Assert.True(clone.EnableExperimentalMode); } [Fact] @@ -456,23 +456,23 @@ public void ResumeSessionConfig_Clone_PreservesEnableSessionTelemetryDefault() } [Fact] - public void SessionConfig_Clone_PreservesIsExperimentalModeDefault() + public void SessionConfig_Clone_PreservesEnableExperimentalModeDefault() { var original = new SessionConfig(); var clone = original.Clone(); - Assert.Null(clone.IsExperimentalMode); + Assert.Null(clone.EnableExperimentalMode); } [Fact] - public void ResumeSessionConfig_Clone_PreservesIsExperimentalModeDefault() + public void ResumeSessionConfig_Clone_PreservesEnableExperimentalModeDefault() { var original = new ResumeSessionConfig(); var clone = original.Clone(); - Assert.Null(clone.IsExperimentalMode); + Assert.Null(clone.EnableExperimentalMode); } [Fact] diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 2d47e783a..071347aa1 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -300,7 +300,7 @@ public void ResumeSessionRequest_CanSerializeEnableSessionTelemetry_WithSdkOptio } [Fact] - public void SessionRequests_CanSerializeIsExperimentalMode_WithSdkOptions() + public void SessionRequests_CanSerializeEnableExperimentalMode_WithSdkOptions() { var options = GetSerializerOptions(); diff --git a/go/client.go b/go/client.go index 07deacf8c..2fe46d33e 100644 --- a/go/client.go +++ b/go/client.go @@ -629,7 +629,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.ToolFilterPrecedence = precedence req.Provider = config.Provider req.EnableSessionTelemetry = config.EnableSessionTelemetry - req.IsExperimentalMode = config.IsExperimentalMode + req.IsExperimentalMode = config.EnableExperimentalMode req.SkipCustomInstructions = config.SkipCustomInstructions req.CustomAgentsLocalOnly = config.CustomAgentsLocalOnly req.CoauthorEnabled = config.CoauthorEnabled @@ -924,7 +924,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.Tools = config.Tools req.Provider = config.Provider req.EnableSessionTelemetry = config.EnableSessionTelemetry - req.IsExperimentalMode = config.IsExperimentalMode + req.IsExperimentalMode = config.EnableExperimentalMode req.SkipCustomInstructions = config.SkipCustomInstructions req.CustomAgentsLocalOnly = config.CustomAgentsLocalOnly req.CoauthorEnabled = config.CoauthorEnabled diff --git a/go/client_test.go b/go/client_test.go index 2e2987a8a..580d4f810 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -1242,8 +1242,8 @@ func TestCreateSessionRequest_RequestMCPApps(t *testing.T) { }) } -func TestSessionRequests_IsExperimentalMode(t *testing.T) { - t.Run("create forwards isExperimentalMode when explicitly false", func(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), } @@ -1260,7 +1260,7 @@ func TestSessionRequests_IsExperimentalMode(t *testing.T) { } }) - t.Run("create omits isExperimentalMode when unset", func(t *testing.T) { + t.Run("create omits enableExperimentalMode when unset", func(t *testing.T) { req := createSessionRequest{} data, _ := json.Marshal(req) var m map[string]any @@ -1270,7 +1270,7 @@ func TestSessionRequests_IsExperimentalMode(t *testing.T) { } }) - t.Run("resume forwards isExperimentalMode when explicitly true", func(t *testing.T) { + t.Run("resume forwards enableExperimentalMode when explicitly true", func(t *testing.T) { req := resumeSessionRequest{ SessionID: "s1", IsExperimentalMode: Bool(true), @@ -1288,7 +1288,7 @@ func TestSessionRequests_IsExperimentalMode(t *testing.T) { } }) - t.Run("resume omits isExperimentalMode when unset", func(t *testing.T) { + t.Run("resume omits enableExperimentalMode when unset", func(t *testing.T) { req := resumeSessionRequest{SessionID: "s1"} data, _ := json.Marshal(req) var m map[string]any diff --git a/go/types.go b/go/types.go index 88ce9aaeb..5aa5f02ab 100644 --- a/go/types.go +++ b/go/types.go @@ -980,12 +980,12 @@ type SessionConfig struct { // regardless of this setting. This is independent of the OpenTelemetry // configuration in ClientOptions.Telemetry. EnableSessionTelemetry *bool - // IsExperimentalMode, when non-nil, overrides the session's experimental + // EnableExperimentalMode, when non-nil, overrides the session's experimental // feature-flag tier resolution. Use Bool(true) to force-enable the // experimental tier for this session, Bool(false) to resolve feature flags // as if experimental were off, or nil to inherit the runtime process // defaults unchanged. - IsExperimentalMode *bool + EnableExperimentalMode *bool // SkipCustomInstructions, when non-nil, controls whether the runtime loads // custom instruction files. See also [ClientOptions.Mode] = [ModeEmpty]. SkipCustomInstructions *bool @@ -1301,12 +1301,12 @@ type ResumeSessionConfig struct { // regardless of this setting. This is independent of the OpenTelemetry // configuration in ClientOptions.Telemetry. EnableSessionTelemetry *bool - // IsExperimentalMode, when non-nil, overrides the resumed session's + // EnableExperimentalMode, when non-nil, overrides the resumed session's // experimental feature-flag tier resolution. Use Bool(true) to force-enable // the experimental tier for this session, Bool(false) to resolve feature // flags as if experimental were off, or nil to inherit the runtime process // defaults unchanged. - IsExperimentalMode *bool + EnableExperimentalMode *bool // SkipCustomInstructions, when non-nil, controls whether the runtime loads // custom instruction files. See also [ClientOptions.Mode] = [ModeEmpty]. SkipCustomInstructions *bool diff --git a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java index 77274a844..4c8671816 100644 --- a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -114,7 +114,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setExcludedTools(config.getExcludedTools()); request.setProvider(config.getProvider()); config.getEnableSessionTelemetry().ifPresent(request::setEnableSessionTelemetry); - config.getIsExperimentalMode().ifPresent(request::setIsExperimentalMode); + config.getEnableExperimentalMode().ifPresent(request::setIsExperimentalMode); if (config.getOnUserInputRequest() != null) { request.setRequestUserInput(true); } @@ -226,7 +226,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setExcludedTools(config.getExcludedTools()); request.setProvider(config.getProvider()); config.getEnableSessionTelemetry().ifPresent(request::setEnableSessionTelemetry); - config.getIsExperimentalMode().ifPresent(request::setIsExperimentalMode); + config.getEnableExperimentalMode().ifPresent(request::setIsExperimentalMode); if (config.getOnUserInputRequest() != null) { request.setRequestUserInput(true); } diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index b60cedc40..d6b0245f2 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -46,7 +46,7 @@ public class ResumeSessionConfig { private List excludedTools; private ProviderConfig provider; private Boolean enableSessionTelemetry; - private Boolean isExperimentalMode; + private Boolean enableExperimentalMode; private Boolean skipCustomInstructions; private Boolean customAgentsLocalOnly; private Boolean coauthorEnabled; @@ -306,32 +306,32 @@ public ResumeSessionConfig clearEnableSessionTelemetry() { * inherit the runtime process defaults unchanged */ @JsonIgnore - public Optional getIsExperimentalMode() { - return Optional.ofNullable(isExperimentalMode); + public Optional getEnableExperimentalMode() { + return Optional.ofNullable(enableExperimentalMode); } /** * Overrides the resumed session's experimental feature-flag tier resolution. * - * @param isExperimentalMode + * @param enableExperimentalMode * {@code true} to force-enable the experimental tier for this * session, {@code false} to resolve feature flags as if experimental * were off * @return this config for method chaining */ - public ResumeSessionConfig setIsExperimentalMode(boolean isExperimentalMode) { - this.isExperimentalMode = isExperimentalMode; + public ResumeSessionConfig setEnableExperimentalMode(boolean enableExperimentalMode) { + this.enableExperimentalMode = enableExperimentalMode; return this; } /** - * Clears the isExperimentalMode setting, reverting to the runtime default + * Clears the enableExperimentalMode setting, reverting to the runtime default * behavior. * * @return this instance for method chaining */ - public ResumeSessionConfig clearIsExperimentalMode() { - this.isExperimentalMode = null; + public ResumeSessionConfig clearEnableExperimentalMode() { + this.enableExperimentalMode = null; return this; } @@ -1565,7 +1565,7 @@ public ResumeSessionConfig clone() { copy.excludedTools = this.excludedTools != null ? new ArrayList<>(this.excludedTools) : null; copy.provider = this.provider; copy.enableSessionTelemetry = this.enableSessionTelemetry; - copy.isExperimentalMode = this.isExperimentalMode; + copy.enableExperimentalMode = this.enableExperimentalMode; copy.reasoningEffort = this.reasoningEffort; copy.reasoningSummary = this.reasoningSummary; copy.contextTier = this.contextTier; diff --git a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java index e681001dd..5731b6f19 100644 --- a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -50,7 +50,7 @@ public class SessionConfig { private List excludedTools; private ProviderConfig provider; private Boolean enableSessionTelemetry; - private Boolean isExperimentalMode; + private Boolean enableExperimentalMode; private Boolean skipCustomInstructions; private Boolean customAgentsLocalOnly; private Boolean coauthorEnabled; @@ -407,32 +407,32 @@ public SessionConfig clearEnableSessionTelemetry() { * inherit the runtime process defaults unchanged */ @JsonIgnore - public Optional getIsExperimentalMode() { - return Optional.ofNullable(isExperimentalMode); + public Optional getEnableExperimentalMode() { + return Optional.ofNullable(enableExperimentalMode); } /** * Overrides the session's experimental feature-flag tier resolution. * - * @param isExperimentalMode + * @param enableExperimentalMode * {@code true} to force-enable the experimental tier for this * session, {@code false} to resolve feature flags as if experimental * were off * @return this config instance for method chaining */ - public SessionConfig setIsExperimentalMode(boolean isExperimentalMode) { - this.isExperimentalMode = isExperimentalMode; + public SessionConfig setEnableExperimentalMode(boolean enableExperimentalMode) { + this.enableExperimentalMode = enableExperimentalMode; return this; } /** - * Clears the isExperimentalMode setting, reverting to the runtime default + * Clears the enableExperimentalMode setting, reverting to the runtime default * behavior. * * @return this instance for method chaining */ - public SessionConfig clearIsExperimentalMode() { - this.isExperimentalMode = null; + public SessionConfig clearEnableExperimentalMode() { + this.enableExperimentalMode = null; return this; } @@ -1688,7 +1688,7 @@ public SessionConfig clone() { copy.excludedTools = this.excludedTools != null ? new ArrayList<>(this.excludedTools) : null; copy.provider = this.provider; copy.enableSessionTelemetry = this.enableSessionTelemetry; - copy.isExperimentalMode = this.isExperimentalMode; + copy.enableExperimentalMode = this.enableExperimentalMode; copy.skipCustomInstructions = this.skipCustomInstructions; copy.customAgentsLocalOnly = this.customAgentsLocalOnly; copy.coauthorEnabled = this.coauthorEnabled; diff --git a/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java index 6b0604ac5..b9cf5d9ef 100644 --- a/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java +++ b/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java @@ -99,14 +99,14 @@ void testBuildCreateRequestSetsReasoningSummary() { } @Test - void testBuildCreateRequestSetsIsExperimentalMode() { - var config = new SessionConfig().setIsExperimentalMode(false); + void testBuildCreateRequestSetsEnableExperimentalMode() { + var config = new SessionConfig().setEnableExperimentalMode(false); CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); assertFalse(request.getIsExperimentalMode()); } @Test - void testBuildCreateRequestOmitsIsExperimentalModeWhenNotSet() { + void testBuildCreateRequestOmitsEnableExperimentalModeWhenNotSet() { CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(new SessionConfig()); assertNull(request.getIsExperimentalMode()); } @@ -190,14 +190,14 @@ void testBuildResumeRequestOmitsEnableSessionTelemetryWhenNotSet() { } @Test - void testBuildResumeRequestSetsIsExperimentalMode() { - var config = new ResumeSessionConfig().setIsExperimentalMode(true); + void testBuildResumeRequestSetsEnableExperimentalMode() { + var config = new ResumeSessionConfig().setEnableExperimentalMode(true); ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); assertTrue(request.getIsExperimentalMode()); } @Test - void testBuildResumeRequestOmitsIsExperimentalModeWhenNotSet() { + void testBuildResumeRequestOmitsEnableExperimentalModeWhenNotSet() { var config = new ResumeSessionConfig(); ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); assertNull(request.getIsExperimentalMode()); diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index bd697f06f..18da87865 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -1100,7 +1100,7 @@ export class CopilotClient { clientName: config.clientName, reasoningEffort: config.reasoningEffort, reasoningSummary: config.reasoningSummary, - isExperimentalMode: config.isExperimentalMode, + isExperimentalMode: config.enableExperimentalMode, contextTier: config.contextTier, tools: config.tools?.map((tool) => ({ name: tool.name, @@ -1282,7 +1282,7 @@ export class CopilotClient { model: config.model, reasoningEffort: config.reasoningEffort, reasoningSummary: config.reasoningSummary, - isExperimentalMode: config.isExperimentalMode, + isExperimentalMode: config.enableExperimentalMode, contextTier: config.contextTier, systemMessage: wireSystemMessage, availableTools: toolFilterOptions.availableTools, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index d255e45b2..ec6f41c68 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1593,7 +1593,7 @@ export interface SessionConfigBase { * `false` to resolve feature flags as if experimental were off. Leave * unset to inherit the runtime process defaults unchanged. */ - isExperimentalMode?: boolean; + enableExperimentalMode?: boolean; /** * Context window tier for models that support it. Use "long_context" to pin diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 6a1e2ce23..84c544da6 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -138,7 +138,7 @@ describe("CopilotClient", () => { expect(resumePayload.reasoningSummary).toBe("none"); }); - it("forwards isExperimentalMode in session.create and session.resume", async () => { + it("forwards enableExperimentalMode in session.create and session.resume", async () => { const client = new CopilotClient(); await client.start(); onTestFinished(() => client.forceStop()); @@ -153,11 +153,11 @@ describe("CopilotClient", () => { const session = await client.createSession({ onPermissionRequest: approveAll, - isExperimentalMode: false, + enableExperimentalMode: false, }); await client.resumeSession(session.sessionId, { onPermissionRequest: approveAll, - isExperimentalMode: true, + enableExperimentalMode: true, }); const createPayload = spy.mock.calls.find( diff --git a/python/copilot/client.py b/python/copilot/client.py index 831e7164f..a00fd71bd 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -1563,7 +1563,7 @@ async def create_session( client_name: str | None = None, reasoning_effort: ReasoningEffort | None = None, reasoning_summary: ReasoningSummary | None = None, - is_experimental_mode: bool | None = None, + enable_experimental_mode: bool | None = None, context_tier: ContextTier | None = None, tools: list[Tool] | None = None, system_message: SystemMessageConfig | None = None, @@ -1637,7 +1637,7 @@ async def create_session( reasoning_summary: Reasoning summary mode for supported models. Use ``"none"`` to suppress summary output regardless of whether reasoning is enabled. - is_experimental_mode: Per-session experimental feature-flag tier + enable_experimental_mode: Per-session experimental feature-flag tier override. Set ``True`` to force-enable the experimental tier for this session or ``False`` to resolve feature flags as if experimental were off. Omit to inherit the runtime process @@ -1790,8 +1790,8 @@ async def create_session( payload["reasoningEffort"] = reasoning_effort if reasoning_summary: payload["reasoningSummary"] = reasoning_summary - if is_experimental_mode is not None: - payload["isExperimentalMode"] = is_experimental_mode + if enable_experimental_mode is not None: + payload["isExperimentalMode"] = enable_experimental_mode if context_tier: payload["contextTier"] = context_tier if tool_defs: @@ -2182,7 +2182,7 @@ async def resume_session( commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, enable_mcp_apps: bool = False, - is_experimental_mode: bool | None = None, + enable_experimental_mode: bool | None = None, on_exit_plan_mode_request: ExitPlanModeHandler | None = None, on_auto_mode_switch_request: AutoModeSwitchHandler | None = None, create_session_fs_handler: CreateSessionFsHandler | None = None, @@ -2215,7 +2215,7 @@ async def resume_session( reasoning_summary: Reasoning summary mode for supported models. Use ``"none"`` to suppress summary output regardless of whether reasoning is enabled. - is_experimental_mode: Per-session experimental feature-flag tier + enable_experimental_mode: Per-session experimental feature-flag tier override. Set ``True`` to force-enable the experimental tier for this resumed session or ``False`` to resolve feature flags as if experimental were off. Omit to inherit the runtime process @@ -2369,8 +2369,8 @@ async def resume_session( payload["reasoningEffort"] = reasoning_effort if reasoning_summary: payload["reasoningSummary"] = reasoning_summary - if is_experimental_mode is not None: - payload["isExperimentalMode"] = is_experimental_mode + if enable_experimental_mode is not None: + payload["isExperimentalMode"] = enable_experimental_mode if context_tier: payload["contextTier"] = context_tier if tool_defs: diff --git a/python/test_client.py b/python/test_client.py index 1e2438e48..4aefc52b9 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -138,7 +138,7 @@ async def mock_request(method, params, **kwargs): await client.force_stop() @pytest.mark.asyncio - async def test_create_and_resume_session_forward_is_experimental_mode(self): + async def test_create_and_resume_session_forward_enable_experimental_mode(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -157,12 +157,12 @@ async def mock_request(method, params, **kwargs): client._client.request = mock_request session = await client.create_session( on_permission_request=PermissionHandler.approve_all, - is_experimental_mode=False, + enable_experimental_mode=False, ) await client.resume_session( session.session_id, on_permission_request=PermissionHandler.approve_all, - is_experimental_mode=True, + enable_experimental_mode=True, ) assert captured["session.create"]["isExperimentalMode"] is False diff --git a/rust/src/types.rs b/rust/src/types.rs index 60f61a74d..14ac6ada0 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -1259,7 +1259,7 @@ pub struct SessionConfig { /// affects the feature-flag resolution for this one session. Serializes /// as `isExperimentalMode` and is omitted from the wire when `None`, so /// older CLIs that don't understand it are unaffected. - pub is_experimental_mode: Option, + pub enable_experimental_mode: Option, /// Skill directory paths passed through to the GitHub Copilot CLI. pub skill_directories: Option>, /// Additional directories to search for custom instruction files. @@ -1432,7 +1432,7 @@ impl std::fmt::Debug for SessionConfig { .field("enable_session_store", &self.enable_session_store) .field("enable_skills", &self.enable_skills) .field("enable_mcp_apps", &self.enable_mcp_apps) - .field("is_experimental_mode", &self.is_experimental_mode) + .field("enable_experimental_mode", &self.enable_experimental_mode) .field("skill_directories", &self.skill_directories) .field("instruction_directories", &self.instruction_directories) .field("plugin_directories", &self.plugin_directories) @@ -1532,7 +1532,7 @@ impl Default for SessionConfig { enable_skills: None, embedding_cache_storage: None, enable_mcp_apps: None, - is_experimental_mode: None, + enable_experimental_mode: None, skill_directories: None, instruction_directories: None, plugin_directories: None, @@ -1675,7 +1675,7 @@ impl SessionConfig { request_auto_mode_switch, request_elicitation, request_mcp_apps: self.enable_mcp_apps.unwrap_or(false), - is_experimental_mode: self.is_experimental_mode, + is_experimental_mode: self.enable_experimental_mode, hooks: hooks_flag, skill_directories: self.skill_directories, instruction_directories: self.instruction_directories, @@ -2020,11 +2020,9 @@ impl SessionConfig { self } - /// Disable (`false`) or force-enable (`true`) the experimental feature-flag - /// tier for this session only. `None` (default) inherits the CLI process's - /// flags. Never persists to config. See the field docs for resume caveats. - pub fn with_is_experimental_mode(mut self, is_experimental_mode: bool) -> Self { - self.is_experimental_mode = Some(is_experimental_mode); + /// Set [`enable_experimental_mode`](Self::enable_experimental_mode). + pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self { + self.enable_experimental_mode = Some(enable_experimental_mode); self } @@ -2290,7 +2288,7 @@ pub struct ResumeSessionConfig { /// cold-load path (the session is not already live in-process); an /// already-active session keeps the flags it was created with. Serializes /// as `isExperimentalMode` and is omitted from the wire when `None`. - pub is_experimental_mode: Option, + pub enable_experimental_mode: Option, /// Skill directory paths passed through to the GitHub Copilot CLI on resume. pub skill_directories: Option>, /// Additional directories to search for custom instruction files on @@ -2434,7 +2432,7 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("enable_session_store", &self.enable_session_store) .field("enable_skills", &self.enable_skills) .field("enable_mcp_apps", &self.enable_mcp_apps) - .field("is_experimental_mode", &self.is_experimental_mode) + .field("enable_experimental_mode", &self.enable_experimental_mode) .field("skill_directories", &self.skill_directories) .field("instruction_directories", &self.instruction_directories) .field("plugin_directories", &self.plugin_directories) @@ -2578,7 +2576,7 @@ impl ResumeSessionConfig { request_auto_mode_switch, request_elicitation, request_mcp_apps: self.enable_mcp_apps.unwrap_or(false), - is_experimental_mode: self.is_experimental_mode, + is_experimental_mode: self.enable_experimental_mode, hooks: hooks_flag, skill_directories: self.skill_directories, instruction_directories: self.instruction_directories, @@ -2655,7 +2653,7 @@ impl ResumeSessionConfig { enable_skills: None, embedding_cache_storage: None, enable_mcp_apps: None, - is_experimental_mode: None, + enable_experimental_mode: None, skill_directories: None, instruction_directories: None, plugin_directories: None, @@ -2975,11 +2973,9 @@ impl ResumeSessionConfig { self } - /// Disable (`false`) or force-enable (`true`) the experimental feature-flag - /// tier for this session only. `None` (default) inherits the CLI process's - /// flags. Never persists to config. See the field docs for resume caveats. - pub fn with_is_experimental_mode(mut self, is_experimental_mode: bool) -> Self { - self.is_experimental_mode = Some(is_experimental_mode); + /// Set [`enable_experimental_mode`](Self::enable_experimental_mode). + pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self { + self.enable_experimental_mode = Some(enable_experimental_mode); self } @@ -4504,13 +4500,13 @@ mod tests { } #[test] - fn session_config_is_experimental_mode_serializes_when_set() { - let cfg = SessionConfig::default().with_is_experimental_mode(false); - assert_eq!(cfg.is_experimental_mode, Some(false)); + fn session_config_enable_experimental_mode_serializes_when_set() { + let cfg = SessionConfig::default().with_enable_experimental_mode(false); + assert_eq!(cfg.enable_experimental_mode, Some(false)); let (wire, _runtime) = cfg .into_wire(Some(SessionId::from("experimental-mode"))) - .expect("is_experimental_mode config has no duplicate handlers"); + .expect("enable_experimental_mode config has no duplicate handlers"); assert_eq!(wire.is_experimental_mode, Some(false)); let json = serde_json::to_value(&wire).unwrap(); @@ -4518,9 +4514,9 @@ mod tests { } #[test] - fn session_config_is_experimental_mode_omitted_when_none() { + fn session_config_enable_experimental_mode_omitted_when_none() { let cfg = SessionConfig::default(); - assert_eq!(cfg.is_experimental_mode, None); + assert_eq!(cfg.enable_experimental_mode, None); let (wire, _runtime) = cfg .into_wire(Some(SessionId::from("no-experimental-mode"))) @@ -4532,14 +4528,14 @@ mod tests { } #[test] - fn resume_session_config_is_experimental_mode_serializes_when_set() { + fn resume_session_config_enable_experimental_mode_serializes_when_set() { let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode")) - .with_is_experimental_mode(false); - assert_eq!(cfg.is_experimental_mode, Some(false)); + .with_enable_experimental_mode(false); + assert_eq!(cfg.enable_experimental_mode, Some(false)); let (wire, _runtime) = cfg .into_wire() - .expect("resume is_experimental_mode config has no duplicate handlers"); + .expect("resume enable_experimental_mode config has no duplicate handlers"); assert_eq!(wire.is_experimental_mode, Some(false)); let json = serde_json::to_value(&wire).unwrap(); @@ -4547,9 +4543,9 @@ mod tests { } #[test] - fn resume_session_config_is_experimental_mode_omitted_when_none() { + fn resume_session_config_enable_experimental_mode_omitted_when_none() { let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode")); - assert_eq!(cfg.is_experimental_mode, None); + assert_eq!(cfg.enable_experimental_mode, None); let (wire, _runtime) = cfg .into_wire() From b0e6ce6bc0dbcac107c34ff3bb44dad97e7e41dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:20:39 +0000 Subject: [PATCH 5/6] Align experimental mode defaults across SDKs Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com> --- dotnet/src/Client.cs | 1 + dotnet/src/Types.cs | 8 +-- dotnet/test/Unit/SerializationTests.cs | 38 ++++++++++ go/mode_empty.go | 8 +++ go/toolset_test.go | 36 ++++++++++ go/types.go | 16 ++--- .../com/github/copilot/CopilotClient.java | 10 ++- .../github/copilot/SessionRequestBuilder.java | 24 ++++++- .../copilot/rpc/ResumeSessionConfig.java | 18 +++-- .../com/github/copilot/rpc/SessionConfig.java | 18 +++-- .../copilot/SessionRequestBuilderTest.java | 15 ++++ nodejs/src/client.ts | 9 ++- nodejs/src/types.ts | 7 +- nodejs/test/client.test.ts | 61 ++++++++++++++++ python/copilot/_mode.py | 8 +++ python/copilot/client.py | 23 +++--- python/test_client.py | 70 +++++++++++++++++++ rust/src/mode.rs | 33 +++++++++ rust/src/session.rs | 4 ++ rust/src/types.rs | 30 ++------ 20 files changed, 357 insertions(+), 80 deletions(-) diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index b00cd52c0..9787b8386 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -664,6 +664,7 @@ private void ApplyConfigDefaultsForMode(SessionConfigBase config) { if (_options.Mode == CopilotClientMode.Empty) { + config.EnableExperimentalMode ??= false; config.EnableSessionTelemetry ??= false; config.SkipEmbeddingRetrieval ??= true; config.EmbeddingCacheStorage ??= EmbeddingCacheStorageMode.InMemory; diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 28c9cafd3..df17af93c 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2641,13 +2641,11 @@ protected SessionConfigBase(SessionConfigBase? other) public bool? EnableSessionTelemetry { get; set; } /// - /// Overrides the session's experimental feature-flag tier resolution. + /// Controls whether the session enables experimental features. /// /// - /// Set to to force-enable the experimental tier for this - /// session, or to resolve feature flags as if - /// experimental were off. Leave to inherit the runtime - /// process defaults unchanged. + /// Defaults to in . + /// Otherwise, the runtime decides when left . /// public bool? EnableExperimentalMode { get; set; } diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 071347aa1..28fdac677 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -333,6 +333,35 @@ public void SessionRequests_CanSerializeEnableExperimentalMode_WithSdkOptions() Assert.False(resumeOmittedRoot.TryGetProperty("isExperimentalMode", out _)); } + [Fact] + public void ApplyConfigDefaultsForMode_EmptyDefaultsEnableExperimentalModeFalse() + { + var client = new CopilotClient(new CopilotClientOptions + { + Mode = CopilotClientMode.Empty, + BaseDirectory = System.IO.Path.GetTempPath(), + }); + var config = new SessionConfig(); + + InvokeApplyConfigDefaultsForMode(client, config); + + Assert.False(config.EnableExperimentalMode); + } + + [Fact] + public void ApplyConfigDefaultsForMode_CopilotCliLeavesEnableExperimentalModeNull() + { + var client = new CopilotClient(new CopilotClientOptions + { + Mode = CopilotClientMode.CopilotCli, + }); + var config = new ResumeSessionConfig(); + + InvokeApplyConfigDefaultsForMode(client, config); + + Assert.Null(config.EnableExperimentalMode); + } + [Fact] public void CreateSessionRequest_CanSerializeEnableOnDemandInstructionDiscovery_WithSdkOptions() { @@ -602,6 +631,15 @@ private static Type GetNestedType(Type containingType, string name) return type!; } + private static void InvokeApplyConfigDefaultsForMode(CopilotClient client, SessionConfigBase config) + { + var method = typeof(CopilotClient).GetMethod( + "ApplyConfigDefaultsForMode", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + Assert.NotNull(method); + method!.Invoke(client, [config]); + } + [Fact] public void HooksInvokeResponse_SerializesBoxedJsonElement_AsOutput() { diff --git a/go/mode_empty.go b/go/mode_empty.go index 05b7e28bb..159f83e91 100644 --- a/go/mode_empty.go +++ b/go/mode_empty.go @@ -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 @@ -163,6 +167,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 diff --git a/go/toolset_test.go b/go/toolset_test.go index 96d90f1d1..f9ff7f0b0 100644 --- a/go/toolset_test.go +++ b/go/toolset_test.go @@ -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 @@ -362,3 +380,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") + } +} diff --git a/go/types.go b/go/types.go index 5aa5f02ab..a8831a513 100644 --- a/go/types.go +++ b/go/types.go @@ -980,11 +980,9 @@ type SessionConfig struct { // regardless of this setting. This is independent of the OpenTelemetry // configuration in ClientOptions.Telemetry. EnableSessionTelemetry *bool - // EnableExperimentalMode, when non-nil, overrides the session's experimental - // feature-flag tier resolution. Use Bool(true) to force-enable the - // experimental tier for this session, Bool(false) to resolve feature flags - // as if experimental were off, or nil to inherit the runtime process - // defaults unchanged. + // EnableExperimentalMode controls whether the session enables experimental + // features. When nil, it defaults to false in [ModeEmpty]; otherwise the + // runtime decides. EnableExperimentalMode *bool // SkipCustomInstructions, when non-nil, controls whether the runtime loads // custom instruction files. See also [ClientOptions.Mode] = [ModeEmpty]. @@ -1301,11 +1299,9 @@ type ResumeSessionConfig struct { // regardless of this setting. This is independent of the OpenTelemetry // configuration in ClientOptions.Telemetry. EnableSessionTelemetry *bool - // EnableExperimentalMode, when non-nil, overrides the resumed session's - // experimental feature-flag tier resolution. Use Bool(true) to force-enable - // the experimental tier for this session, Bool(false) to resolve feature - // flags as if experimental were off, or nil to inherit the runtime process - // defaults unchanged. + // EnableExperimentalMode controls whether the session enables experimental + // features. When nil, it defaults to false in [ModeEmpty]; otherwise the + // runtime decides. EnableExperimentalMode *bool // SkipCustomInstructions, when non-nil, controls whether the runtime loads // custom instruction files. See also [ClientOptions.Mode] = [ModeEmpty]. diff --git a/java/src/main/java/com/github/copilot/CopilotClient.java b/java/src/main/java/com/github/copilot/CopilotClient.java index 9e4a776ad..85ed3c2e9 100644 --- a/java/src/main/java/com/github/copilot/CopilotClient.java +++ b/java/src/main/java/com/github/copilot/CopilotClient.java @@ -513,7 +513,7 @@ public CompletableFuture createSession(SessionConfig config) { registeredIdHolder[0] = localSessionId; } - var request = SessionRequestBuilder.buildCreateRequest(config, localSessionId); + var request = SessionRequestBuilder.buildCreateRequest(config, localSessionId, options.getMode()); if (extracted.wireSystemMessage() != config.getSystemMessage()) { request.setSystemMessage(extracted.wireSystemMessage()); } @@ -551,6 +551,9 @@ public CompletableFuture createSession(SessionConfig config) { if (request.getEnableSkills() == null) { request.setEnableSkills(false); } + if (request.getIsExperimentalMode() == null) { + request.setIsExperimentalMode(false); + } if (request.getMcpOAuthTokenStorage() == null) { request.setMcpOAuthTokenStorage("in-memory"); } @@ -651,7 +654,7 @@ public CompletableFuture resumeSession(String sessionId, ResumeS session.registerTransformCallbacks(extracted.transformCallbacks()); } - var request = SessionRequestBuilder.buildResumeRequest(sessionId, config); + var request = SessionRequestBuilder.buildResumeRequest(sessionId, config, options.getMode()); if (extracted.wireSystemMessage() != config.getSystemMessage()) { request.setSystemMessage(extracted.wireSystemMessage()); } @@ -687,6 +690,9 @@ public CompletableFuture resumeSession(String sessionId, ResumeS if (request.getEnableSkills() == null) { request.setEnableSkills(false); } + if (request.getIsExperimentalMode() == null) { + request.setIsExperimentalMode(false); + } if (request.getMcpOAuthTokenStorage() == null) { request.setMcpOAuthTokenStorage("in-memory"); } diff --git a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java index 4c8671816..7ef6cbbd2 100644 --- a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -6,11 +6,13 @@ import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.function.Function; import com.github.copilot.rpc.CreateSessionRequest; import com.github.copilot.rpc.CommandWireDefinition; +import com.github.copilot.rpc.CopilotClientMode; import com.github.copilot.rpc.ResumeSessionConfig; import com.github.copilot.rpc.ResumeSessionRequest; import com.github.copilot.rpc.SectionOverride; @@ -93,6 +95,10 @@ static ExtractedTransforms extractTransformCallbacks(SystemMessageConfig systemM * @return the built request object */ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sessionId) { + return buildCreateRequest(config, sessionId, CopilotClientMode.COPILOT_CLI); + } + + static CreateSessionRequest buildCreateRequest(SessionConfig config, String sessionId, CopilotClientMode mode) { var request = new CreateSessionRequest(); // Always request permission callbacks to enable deny-by-default behavior request.setRequestPermission(true); @@ -114,7 +120,8 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setExcludedTools(config.getExcludedTools()); request.setProvider(config.getProvider()); config.getEnableSessionTelemetry().ifPresent(request::setEnableSessionTelemetry); - config.getEnableExperimentalMode().ifPresent(request::setIsExperimentalMode); + experimentalModeForMode(mode, config.getEnableExperimentalMode().orElse(null)) + .ifPresent(request::setIsExperimentalMode); if (config.getOnUserInputRequest() != null) { request.setRequestUserInput(true); } @@ -204,6 +211,11 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config) { * @return the built request object */ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionConfig config) { + return buildResumeRequest(sessionId, config, CopilotClientMode.COPILOT_CLI); + } + + static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionConfig config, + CopilotClientMode mode) { var request = new ResumeSessionRequest(); request.setSessionId(sessionId); // Always request permission callbacks to enable deny-by-default behavior @@ -226,7 +238,8 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setExcludedTools(config.getExcludedTools()); request.setProvider(config.getProvider()); config.getEnableSessionTelemetry().ifPresent(request::setEnableSessionTelemetry); - config.getEnableExperimentalMode().ifPresent(request::setIsExperimentalMode); + experimentalModeForMode(mode, config.getEnableExperimentalMode().orElse(null)) + .ifPresent(request::setIsExperimentalMode); if (config.getOnUserInputRequest() != null) { request.setRequestUserInput(true); } @@ -292,6 +305,13 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo return request; } + private static Optional experimentalModeForMode(CopilotClientMode mode, Boolean supplied) { + if (mode == CopilotClientMode.EMPTY) { + return Optional.of(supplied != null ? supplied : false); + } + return Optional.ofNullable(supplied); + } + /** * Configures a session with handlers from the given config. * diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index d6b0245f2..7c36f6d6c 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -299,11 +299,10 @@ public ResumeSessionConfig clearEnableSessionTelemetry() { } /** - * Overrides the resumed session's experimental feature-flag tier resolution. + * Controls whether the session enables experimental features. * - * @return {@code true} to force-enable the experimental tier, {@code false} to - * resolve feature flags as if experimental were off, or empty to - * inherit the runtime process defaults unchanged + * @return {@code true} when experimental features are enabled, {@code false} + * when they are disabled, or empty to use the mode-specific default */ @JsonIgnore public Optional getEnableExperimentalMode() { @@ -311,12 +310,11 @@ public Optional getEnableExperimentalMode() { } /** - * Overrides the resumed session's experimental feature-flag tier resolution. + * Controls whether the session enables experimental features. * * @param enableExperimentalMode - * {@code true} to force-enable the experimental tier for this - * session, {@code false} to resolve feature flags as if experimental - * were off + * {@code true} to enable experimental features; {@code false} to + * disable them * @return this config for method chaining */ public ResumeSessionConfig setEnableExperimentalMode(boolean enableExperimentalMode) { @@ -325,8 +323,8 @@ public ResumeSessionConfig setEnableExperimentalMode(boolean enableExperimentalM } /** - * Clears the enableExperimentalMode setting, reverting to the runtime default - * behavior. + * Clears the enableExperimentalMode setting. In {@link CopilotClientMode#EMPTY + * EMPTY} mode this defaults to {@code false}; otherwise the runtime decides. * * @return this instance for method chaining */ diff --git a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java index 5731b6f19..e78520322 100644 --- a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -400,11 +400,10 @@ public SessionConfig clearEnableSessionTelemetry() { } /** - * Overrides the session's experimental feature-flag tier resolution. + * Controls whether the session enables experimental features. * - * @return {@code true} to force-enable the experimental tier, {@code false} to - * resolve feature flags as if experimental were off, or empty to - * inherit the runtime process defaults unchanged + * @return {@code true} when experimental features are enabled, {@code false} + * when they are disabled, or empty to use the mode-specific default */ @JsonIgnore public Optional getEnableExperimentalMode() { @@ -412,12 +411,11 @@ public Optional getEnableExperimentalMode() { } /** - * Overrides the session's experimental feature-flag tier resolution. + * Controls whether the session enables experimental features. * * @param enableExperimentalMode - * {@code true} to force-enable the experimental tier for this - * session, {@code false} to resolve feature flags as if experimental - * were off + * {@code true} to enable experimental features; {@code false} to + * disable them * @return this config instance for method chaining */ public SessionConfig setEnableExperimentalMode(boolean enableExperimentalMode) { @@ -426,8 +424,8 @@ public SessionConfig setEnableExperimentalMode(boolean enableExperimentalMode) { } /** - * Clears the enableExperimentalMode setting, reverting to the runtime default - * behavior. + * Clears the enableExperimentalMode setting. In {@link CopilotClientMode#EMPTY + * EMPTY} mode this defaults to {@code false}; otherwise the runtime decides. * * @return this instance for method chaining */ diff --git a/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java index b9cf5d9ef..246c0c359 100644 --- a/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java +++ b/java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java @@ -15,6 +15,7 @@ import com.github.copilot.rpc.AutoModeSwitchResponse; import com.github.copilot.rpc.CloudSessionOptions; import com.github.copilot.rpc.CloudSessionRepository; +import com.github.copilot.rpc.CopilotClientMode; import com.github.copilot.rpc.CreateSessionRequest; import com.github.copilot.rpc.DefaultAgentConfig; import com.github.copilot.rpc.ElicitationHandler; @@ -111,6 +112,13 @@ void testBuildCreateRequestOmitsEnableExperimentalModeWhenNotSet() { assertNull(request.getIsExperimentalMode()); } + @Test + void testBuildCreateRequestDefaultsEnableExperimentalModeFalseInEmptyMode() { + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(new SessionConfig(), "sid-empty", + CopilotClientMode.EMPTY); + assertFalse(request.getIsExperimentalMode()); + } + @Test void testBuildCreateRequestSetsContextTier() { var config = new SessionConfig().setContextTier("long_context"); @@ -203,6 +211,13 @@ void testBuildResumeRequestOmitsEnableExperimentalModeWhenNotSet() { assertNull(request.getIsExperimentalMode()); } + @Test + void testBuildResumeRequestDefaultsEnableExperimentalModeFalseInEmptyMode() { + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-empty", new ResumeSessionConfig(), + CopilotClientMode.EMPTY); + assertFalse(request.getIsExperimentalMode()); + } + @Test void testBuildResumeRequestWithTools() { var tool = ToolDefinition.create("my_tool", "A tool", Map.of("type", "object"), diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 18da87865..3b842b9d7 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -923,6 +923,11 @@ export class CopilotClient { return {}; } + /** Mode-specific default for enableExperimentalMode. */ + private experimentalModeForMode(supplied: boolean | undefined): boolean | undefined { + return this.options.mode === "empty" ? (supplied ?? false) : supplied; + } + /** * Returns the systemMessage config to use, adjusted for the current mode. * In empty mode we ensure the environment_context section is removed @@ -1100,7 +1105,7 @@ export class CopilotClient { clientName: config.clientName, reasoningEffort: config.reasoningEffort, reasoningSummary: config.reasoningSummary, - isExperimentalMode: config.enableExperimentalMode, + isExperimentalMode: this.experimentalModeForMode(config.enableExperimentalMode), contextTier: config.contextTier, tools: config.tools?.map((tool) => ({ name: tool.name, @@ -1282,7 +1287,7 @@ export class CopilotClient { model: config.model, reasoningEffort: config.reasoningEffort, reasoningSummary: config.reasoningSummary, - isExperimentalMode: config.enableExperimentalMode, + isExperimentalMode: this.experimentalModeForMode(config.enableExperimentalMode), contextTier: config.contextTier, systemMessage: wireSystemMessage, availableTools: toolFilterOptions.availableTools, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index ec6f41c68..a0f6bba3b 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1587,11 +1587,8 @@ export interface SessionConfigBase { reasoningSummary?: ReasoningSummary; /** - * Per-session experimental feature-flag tier override. - * - * Set to `true` to force-enable the experimental tier for this session, or - * `false` to resolve feature flags as if experimental were off. Leave - * unset to inherit the runtime process defaults unchanged. + * Controls whether the session enables experimental features. + * Defaults to `false` in `"empty"` mode; otherwise the runtime decides when unset. */ enableExperimentalMode?: boolean; diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 84c544da6..a9a353355 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -1,4 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { describe, expect, it, onTestFinished, vi } from "vitest"; import { approveAll, @@ -170,6 +173,64 @@ describe("CopilotClient", () => { expect(resumePayload.isExperimentalMode).toBe(true); }); + it("defaults enableExperimentalMode by client mode", async () => { + const baseDirectory = mkdtempSync(join(tmpdir(), "copilot-sdk-node-empty-")); + const emptyClient = new CopilotClient({ mode: "empty", baseDirectory }); + await emptyClient.start(); + onTestFinished(() => emptyClient.forceStop()); + + const emptySpy = vi + .spyOn((emptyClient as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") return { sessionId: params.sessionId }; + if (method === "session.options.update") return {}; + throw new Error(`Unexpected method: ${method}`); + }); + + const emptySession = await emptyClient.createSession({ + onPermissionRequest: approveAll, + availableTools: [], + }); + await emptyClient.resumeSession(emptySession.sessionId, { + onPermissionRequest: approveAll, + availableTools: [], + }); + + const emptyCreatePayload = emptySpy.mock.calls.find( + ([method]) => method === "session.create" + )![1] as any; + const emptyResumePayload = emptySpy.mock.calls.find( + ([method]) => method === "session.resume" + )![1] as any; + expect(emptyCreatePayload.isExperimentalMode).toBe(false); + expect(emptyResumePayload.isExperimentalMode).toBe(false); + + const cliClient = new CopilotClient(); + await cliClient.start(); + onTestFinished(() => cliClient.forceStop()); + + const cliSpy = vi + .spyOn((cliClient as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + + const cliSession = await cliClient.createSession({ + onPermissionRequest: approveAll, + }); + await cliClient.resumeSession(cliSession.sessionId, { + onPermissionRequest: approveAll, + }); + + const cliCreatePayload = cliSpy.mock.calls.find(([method]) => method === "session.create")![1] as any; + const cliResumePayload = cliSpy.mock.calls.find(([method]) => method === "session.resume")![1] as any; + expect(cliCreatePayload.isExperimentalMode).toBeUndefined(); + expect(cliResumePayload.isExperimentalMode).toBeUndefined(); + }); + it("forwards contextTier in session.create and session.resume", async () => { const client = new CopilotClient(); await client.start(); diff --git a/python/copilot/_mode.py b/python/copilot/_mode.py index 9323423f6..8d0ed9a3e 100644 --- a/python/copilot/_mode.py +++ b/python/copilot/_mode.py @@ -248,6 +248,14 @@ def _enable_skills_default( return _empty_mode_bool_default(mode, supplied, False) +def _enable_experimental_mode_default( + mode: CopilotClientMode | None, + supplied: bool | None, +) -> bool | None: + """Empty mode defaults experimental mode to False; caller value wins.""" + return _empty_mode_bool_default(mode, supplied, False) + + def _mcp_oauth_token_storage_default( mode: CopilotClientMode | None, supplied: Literal["persistent", "in-memory"] | None, diff --git a/python/copilot/client.py b/python/copilot/client.py index a00fd71bd..795b1b496 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -41,6 +41,7 @@ _enable_file_hooks_default, _enable_host_git_operations_default, _enable_on_demand_instruction_discovery_default, + _enable_experimental_mode_default, _enable_session_store_default, _enable_session_telemetry_default, _enable_skills_default, @@ -1637,11 +1638,9 @@ async def create_session( reasoning_summary: Reasoning summary mode for supported models. Use ``"none"`` to suppress summary output regardless of whether reasoning is enabled. - enable_experimental_mode: Per-session experimental feature-flag tier - override. Set ``True`` to force-enable the experimental tier for - this session or ``False`` to resolve feature flags as if - experimental were off. Omit to inherit the runtime process - defaults unchanged. + enable_experimental_mode: Controls whether the session enables + experimental features. Defaults to ``False`` in ``"empty"`` + mode; otherwise the runtime decides when omitted. context_tier: Context window tier for models that support it. Use ``"long_context"`` to pin the session to the long-context tier. tools: Custom tools to register with the session. @@ -1780,6 +1779,9 @@ async def create_session( ) enable_session_store = _enable_session_store_default(mode, enable_session_store) enable_skills = _enable_skills_default(mode, enable_skills) + enable_experimental_mode = _enable_experimental_mode_default( + mode, enable_experimental_mode + ) payload: dict[str, Any] = {} if model: @@ -2215,11 +2217,9 @@ async def resume_session( reasoning_summary: Reasoning summary mode for supported models. Use ``"none"`` to suppress summary output regardless of whether reasoning is enabled. - enable_experimental_mode: Per-session experimental feature-flag tier - override. Set ``True`` to force-enable the experimental tier for - this resumed session or ``False`` to resolve feature flags as if - experimental were off. Omit to inherit the runtime process - defaults unchanged. + enable_experimental_mode: Controls whether the session enables + experimental features. Defaults to ``False`` in ``"empty"`` + mode; otherwise the runtime decides when omitted. context_tier: Context window tier for models that support it. Use ``"long_context"`` to pin the session to the long-context tier. tools: Custom tools to register with the session. @@ -2358,6 +2358,9 @@ async def resume_session( ) enable_session_store = _enable_session_store_default(mode, enable_session_store) enable_skills = _enable_skills_default(mode, enable_skills) + enable_experimental_mode = _enable_experimental_mode_default( + mode, enable_experimental_mode + ) payload: dict[str, Any] = {"sessionId": session_id} diff --git a/python/test_client.py b/python/test_client.py index 4aefc52b9..bfe3d067f 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -5,6 +5,7 @@ """ from datetime import UTC, datetime +from tempfile import TemporaryDirectory from unittest.mock import AsyncMock, patch import pytest @@ -170,6 +171,75 @@ async def mock_request(method, params, **kwargs): finally: await client.force_stop() + @pytest.mark.asyncio + async def test_create_and_resume_session_default_enable_experimental_mode_by_mode(self): + with TemporaryDirectory() as base_directory: + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + mode="empty", + base_directory=base_directory, + ) + await client.start() + try: + captured = {} + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method in ("session.create", "session.resume"): + result = {"sessionId": params.get("sessionId") or "session-1"} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + if method == "session.options.update": + return {"success": True} + return {} + + client._client.request = mock_request + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + available_tools=[], + ) + await client.resume_session( + session.session_id, + on_permission_request=PermissionHandler.approve_all, + available_tools=[], + ) + + assert captured["session.create"]["isExperimentalMode"] is False + assert captured["session.resume"]["isExperimentalMode"] is False + finally: + await client.force_stop() + + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured = {} + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method in ("session.create", "session.resume"): + result = {"sessionId": params.get("sessionId") or "session-1"} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + return {} + + client._client.request = mock_request + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + ) + await client.resume_session( + session.session_id, + on_permission_request=PermissionHandler.approve_all, + ) + + assert "isExperimentalMode" not in captured["session.create"] + assert "isExperimentalMode" not in captured["session.resume"] + finally: + await client.force_stop() + @pytest.mark.asyncio async def test_create_and_resume_session_forward_context_tier(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) diff --git a/rust/src/mode.rs b/rust/src/mode.rs index c86b03071..3294f49ef 100644 --- a/rust/src/mode.rs +++ b/rust/src/mode.rs @@ -266,6 +266,18 @@ pub(crate) fn system_message_for_mode( } } +/// Returns the `enable_experimental_mode` value to send for the given mode. +pub(crate) fn experimental_mode_for_mode( + mode: ClientMode, + supplied: Option, +) -> Option { + if mode == ClientMode::Empty { + Some(supplied.unwrap_or(false)) + } else { + supplied + } +} + #[cfg(test)] mod tests { use super::*; @@ -464,4 +476,25 @@ mod tests { let env = secs.get("environment_context").unwrap(); assert_eq!(env.action.as_deref(), Some("remove")); } + + #[test] + fn experimental_mode_defaults_false_in_empty_mode() { + assert_eq!(experimental_mode_for_mode(ClientMode::Empty, None), Some(false)); + assert_eq!( + experimental_mode_for_mode(ClientMode::Empty, Some(true)), + Some(true) + ); + assert_eq!( + experimental_mode_for_mode(ClientMode::Empty, Some(false)), + Some(false) + ); + } + + #[test] + fn experimental_mode_remains_runtime_controlled_in_copilot_cli_mode() { + assert_eq!( + experimental_mode_for_mode(ClientMode::CopilotCli, None), + None + ); + } } diff --git a/rust/src/session.rs b/rust/src/session.rs index 6fc7a1857..10a6bd8cb 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -837,6 +837,8 @@ impl Client { crate::mode::validate_tool_filter_list("excluded_tools", config.excluded_tools.as_deref())?; config.system_message = crate::mode::system_message_for_mode(mode, config.system_message.take()); + config.enable_experimental_mode = + crate::mode::experimental_mode_for_mode(mode, config.enable_experimental_mode); if mode == crate::ClientMode::Empty { if config.enable_session_telemetry.is_none() { config.enable_session_telemetry = Some(false); @@ -1092,6 +1094,8 @@ impl Client { crate::mode::validate_tool_filter_list("excluded_tools", config.excluded_tools.as_deref())?; config.system_message = crate::mode::system_message_for_mode(mode, config.system_message.take()); + config.enable_experimental_mode = + crate::mode::experimental_mode_for_mode(mode, config.enable_experimental_mode); if mode == crate::ClientMode::Empty { if config.enable_session_telemetry.is_none() { config.enable_session_telemetry = Some(false); diff --git a/rust/src/types.rs b/rust/src/types.rs index 14ac6ada0..21c61a9d6 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -1246,19 +1246,10 @@ pub struct SessionConfig { /// /// Defaults to `None` (treated as `false`). pub enable_mcp_apps: Option, - /// Disable, force-enable, or inherit the experimental feature-flag tier - /// for this session only. + /// Controls whether the session enables experimental features. /// - /// - `Some(false)` — the runtime re-resolves this session's feature flags - /// as if experimental mode were off, stripping experimental-tier flags. - /// - `Some(true)` — force-enables the experimental tier even if the CLI - /// process didn't start with it. - /// - `None` (default) — inherits the CLI process's flags unchanged. - /// - /// This never persists anything to the user's shared config; it only - /// affects the feature-flag resolution for this one session. Serializes - /// as `isExperimentalMode` and is omitted from the wire when `None`, so - /// older CLIs that don't understand it are unaffected. + /// Defaults to `false` in [`crate::ClientMode::Empty`]. Otherwise, the + /// runtime decides when this is `None`. pub enable_experimental_mode: Option, /// Skill directory paths passed through to the GitHub Copilot CLI. pub skill_directories: Option>, @@ -2275,19 +2266,10 @@ pub struct ResumeSessionConfig { /// Enable MCP Apps (SEP-1865) UI passthrough on resume. See /// [`SessionConfig::enable_mcp_apps`]. Defaults to `None` (treated as `false`). pub enable_mcp_apps: Option, - /// Disable, force-enable, or inherit the experimental feature-flag tier - /// for this resumed session only. - /// - /// - `Some(false)` — re-resolves this session's feature flags as if - /// experimental mode were off, stripping experimental-tier flags. - /// - `Some(true)` — force-enables the experimental tier even if the CLI - /// process didn't start with it. - /// - `None` (default) — inherits the CLI process's flags unchanged. + /// Controls whether the session enables experimental features. /// - /// Never persists to config. Note: resume only re-resolves flags on the - /// cold-load path (the session is not already live in-process); an - /// already-active session keeps the flags it was created with. Serializes - /// as `isExperimentalMode` and is omitted from the wire when `None`. + /// Defaults to `false` in [`crate::ClientMode::Empty`]. Otherwise, the + /// runtime decides when this is `None`. pub enable_experimental_mode: Option, /// Skill directory paths passed through to the GitHub Copilot CLI on resume. pub skill_directories: Option>, From a2a56cce6c7ce50d3496096beca5b3d2c2dcea85 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:56:46 +0000 Subject: [PATCH 6/6] Fix CI formatting failures Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com> --- nodejs/test/client.test.ts | 8 ++++++-- python/copilot/client.py | 12 ++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index a9a353355..e434252a6 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -225,8 +225,12 @@ describe("CopilotClient", () => { onPermissionRequest: approveAll, }); - const cliCreatePayload = cliSpy.mock.calls.find(([method]) => method === "session.create")![1] as any; - const cliResumePayload = cliSpy.mock.calls.find(([method]) => method === "session.resume")![1] as any; + const cliCreatePayload = cliSpy.mock.calls.find( + ([method]) => method === "session.create" + )![1] as any; + const cliResumePayload = cliSpy.mock.calls.find( + ([method]) => method === "session.resume" + )![1] as any; expect(cliCreatePayload.isExperimentalMode).toBeUndefined(); expect(cliResumePayload.isExperimentalMode).toBeUndefined(); }); diff --git a/python/copilot/client.py b/python/copilot/client.py index 795b1b496..c42554e71 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -38,10 +38,10 @@ CopilotClientMode, ToolSet, _embedding_cache_storage_default, + _enable_experimental_mode_default, _enable_file_hooks_default, _enable_host_git_operations_default, _enable_on_demand_instruction_discovery_default, - _enable_experimental_mode_default, _enable_session_store_default, _enable_session_telemetry_default, _enable_skills_default, @@ -1779,9 +1779,7 @@ async def create_session( ) enable_session_store = _enable_session_store_default(mode, enable_session_store) enable_skills = _enable_skills_default(mode, enable_skills) - enable_experimental_mode = _enable_experimental_mode_default( - mode, enable_experimental_mode - ) + enable_experimental_mode = _enable_experimental_mode_default(mode, enable_experimental_mode) payload: dict[str, Any] = {} if model: @@ -2142,6 +2140,7 @@ async def resume_session( client_name: str | None = None, reasoning_effort: ReasoningEffort | None = None, reasoning_summary: ReasoningSummary | None = None, + enable_experimental_mode: bool | None = None, context_tier: ContextTier | None = None, tools: list[Tool] | None = None, system_message: SystemMessageConfig | None = None, @@ -2184,7 +2183,6 @@ async def resume_session( commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, enable_mcp_apps: bool = False, - enable_experimental_mode: bool | None = None, on_exit_plan_mode_request: ExitPlanModeHandler | None = None, on_auto_mode_switch_request: AutoModeSwitchHandler | None = None, create_session_fs_handler: CreateSessionFsHandler | None = None, @@ -2358,9 +2356,7 @@ async def resume_session( ) enable_session_store = _enable_session_store_default(mode, enable_session_store) enable_skills = _enable_skills_default(mode, enable_skills) - enable_experimental_mode = _enable_experimental_mode_default( - mode, enable_experimental_mode - ) + enable_experimental_mode = _enable_experimental_mode_default(mode, enable_experimental_mode) payload: dict[str, Any] = {"sessionId": session_id}