diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 8f879043a..e50b8e340 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -622,6 +622,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.Agent, config.ConfigDir, config.EnableConfigDiscovery, + config.EnableOnDemandInstructionDiscovery, config.SkillDirectories, config.DisabledSkills, config.InfiniteSessions, @@ -776,6 +777,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.WorkingDirectory, config.ConfigDir, config.EnableConfigDiscovery, + config.EnableOnDemandInstructionDiscovery, config.DisableResume is true ? true : null, config.Streaming is true ? true : null, config.IncludeSubAgentStreamingEvents, @@ -1992,6 +1994,7 @@ internal record CreateSessionRequest( string? Agent, string? ConfigDir, bool? EnableConfigDiscovery, + bool? EnableOnDemandInstructionDiscovery, IList? SkillDirectories, IList? DisabledSkills, InfiniteSessionConfig? InfiniteSessions, @@ -2046,6 +2049,7 @@ internal record ResumeSessionRequest( string? WorkingDirectory, string? ConfigDir, bool? EnableConfigDiscovery, + bool? EnableOnDemandInstructionDiscovery, bool? DisableResume, bool? Streaming, bool? IncludeSubAgentStreamingEvents, diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index f93051111..edf469457 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2077,6 +2077,7 @@ protected SessionConfig(SessionConfig? other) Agent = other.Agent; DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null; EnableConfigDiscovery = other.EnableConfigDiscovery; + EnableOnDemandInstructionDiscovery = other.EnableOnDemandInstructionDiscovery; ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null; Hooks = other.Hooks; InfiniteSessions = other.InfiniteSessions; @@ -2157,6 +2158,25 @@ protected SessionConfig(SessionConfig? other) /// public bool? EnableConfigDiscovery { get; set; } + /// + /// When , requests on-demand discovery of custom instruction + /// files after the agent successfully reads or views files. Discovered instruction + /// files are treated as model instructions and may influence agent behavior. + /// + /// Runtime-gated: only takes effect when custom instructions are enabled and the + /// connected runtime supports and enables on-demand custom instruction discovery. + /// Otherwise the runtime accepts the option but performs no on-demand instruction + /// discovery. + /// + /// + /// Security: enable only for trusted repositories or workspaces. Discovered + /// instruction files may be stored or replayed with session history. Do not enable + /// for untrusted content, CI jobs processing untrusted forks, or directories + /// writable by untrusted users or processes. + /// + /// + public bool? EnableOnDemandInstructionDiscovery { get; set; } + /// /// Custom tool functions available to the language model during the session. /// @@ -2385,6 +2405,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null; DisableResume = other.DisableResume; EnableConfigDiscovery = other.EnableConfigDiscovery; + EnableOnDemandInstructionDiscovery = other.EnableOnDemandInstructionDiscovery; ContinuePendingWork = other.ContinuePendingWork; ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null; Hooks = other.Hooks; @@ -2543,6 +2564,17 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) /// public bool? EnableConfigDiscovery { get; set; } + /// + /// When , requests on-demand discovery of custom instruction + /// files after the agent successfully reads or views files. See + /// for details. + /// + /// For resumed sessions, omitting this option leaves the existing session setting + /// unchanged; set to disable future on-demand discovery. + /// + /// + public bool? EnableOnDemandInstructionDiscovery { get; set; } + /// /// When true, the session.resume event is not emitted. /// Default: false (resume event is emitted). diff --git a/dotnet/test/Unit/CloneTests.cs b/dotnet/test/Unit/CloneTests.cs index 0816da9b2..d48dc6d73 100644 --- a/dotnet/test/Unit/CloneTests.cs +++ b/dotnet/test/Unit/CloneTests.cs @@ -92,6 +92,7 @@ public void SessionConfig_Clone_CopiesAllProperties() WorkingDirectory = "/workspace", Streaming = true, EnableSessionTelemetry = false, + EnableOnDemandInstructionDiscovery = true, IncludeSubAgentStreamingEvents = false, McpServers = new Dictionary { ["server1"] = new McpStdioServerConfig { Command = "echo" } }, CustomAgents = [new CustomAgentConfig { Name = "agent1", Model = "claude-haiku-4.5" }], @@ -125,6 +126,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.EnableOnDemandInstructionDiscovery, clone.EnableOnDemandInstructionDiscovery); Assert.Equal(original.IncludeSubAgentStreamingEvents, clone.IncludeSubAgentStreamingEvents); Assert.Equal(original.McpServers.Count, clone.McpServers!.Count); Assert.Equal(original.CustomAgents.Count, clone.CustomAgents!.Count); @@ -403,4 +405,50 @@ public void ResumeSessionConfig_Clone_PreservesEnableSessionTelemetryDefault() Assert.Null(clone.EnableSessionTelemetry); } + + [Fact] + public void SessionConfig_Clone_CopiesEnableOnDemandInstructionDiscovery() + { + var original = new SessionConfig + { + EnableOnDemandInstructionDiscovery = false, + }; + + var clone = original.Clone(); + + Assert.False(clone.EnableOnDemandInstructionDiscovery); + } + + [Fact] + public void ResumeSessionConfig_Clone_CopiesEnableOnDemandInstructionDiscovery() + { + var original = new ResumeSessionConfig + { + EnableOnDemandInstructionDiscovery = true, + }; + + var clone = original.Clone(); + + Assert.True(clone.EnableOnDemandInstructionDiscovery); + } + + [Fact] + public void SessionConfig_Clone_PreservesEnableOnDemandInstructionDiscoveryDefault() + { + var original = new SessionConfig(); + + var clone = original.Clone(); + + Assert.Null(clone.EnableOnDemandInstructionDiscovery); + } + + [Fact] + public void ResumeSessionConfig_Clone_PreservesEnableOnDemandInstructionDiscoveryDefault() + { + var original = new ResumeSessionConfig(); + + var clone = original.Clone(); + + Assert.Null(clone.EnableOnDemandInstructionDiscovery); + } } diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 1ca6562f8..ff3c004a7 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -203,6 +203,60 @@ public void ResumeSessionRequest_CanSerializeEnableSessionTelemetry_WithSdkOptio Assert.False(root.GetProperty("enableSessionTelemetry").GetBoolean()); } + [Fact] + public void CreateSessionRequest_CanSerializeEnableOnDemandInstructionDiscovery_WithSdkOptions() + { + var options = GetSerializerOptions(); + var requestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); + + var requestTrue = CreateInternalRequest( + requestType, + ("SessionId", "session-id"), + ("EnableOnDemandInstructionDiscovery", true)); + var rootTrue = JsonDocument.Parse(JsonSerializer.Serialize(requestTrue, requestType, options)).RootElement; + Assert.True(rootTrue.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean()); + + var requestFalse = CreateInternalRequest( + requestType, + ("SessionId", "session-id"), + ("EnableOnDemandInstructionDiscovery", false)); + var rootFalse = JsonDocument.Parse(JsonSerializer.Serialize(requestFalse, requestType, options)).RootElement; + Assert.False(rootFalse.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean()); + + var requestOmitted = CreateInternalRequest( + requestType, + ("SessionId", "session-id")); + var rootOmitted = JsonDocument.Parse(JsonSerializer.Serialize(requestOmitted, requestType, options)).RootElement; + Assert.False(rootOmitted.TryGetProperty("enableOnDemandInstructionDiscovery", out _)); + } + + [Fact] + public void ResumeSessionRequest_CanSerializeEnableOnDemandInstructionDiscovery_WithSdkOptions() + { + var options = GetSerializerOptions(); + var requestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); + + var requestTrue = CreateInternalRequest( + requestType, + ("SessionId", "session-id"), + ("EnableOnDemandInstructionDiscovery", true)); + var rootTrue = JsonDocument.Parse(JsonSerializer.Serialize(requestTrue, requestType, options)).RootElement; + Assert.True(rootTrue.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean()); + + var requestFalse = CreateInternalRequest( + requestType, + ("SessionId", "session-id"), + ("EnableOnDemandInstructionDiscovery", false)); + var rootFalse = JsonDocument.Parse(JsonSerializer.Serialize(requestFalse, requestType, options)).RootElement; + Assert.False(rootFalse.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean()); + + var requestOmitted = CreateInternalRequest( + requestType, + ("SessionId", "session-id")); + var rootOmitted = JsonDocument.Parse(JsonSerializer.Serialize(requestOmitted, requestType, options)).RootElement; + Assert.False(rootOmitted.TryGetProperty("enableOnDemandInstructionDiscovery", out _)); + } + [Fact] public void ResumeSessionRequest_CanSerializeModeRequestFlags_WithSdkOptions() { diff --git a/go/client.go b/go/client.go index 9730fc6d4..e1aefad9c 100644 --- a/go/client.go +++ b/go/client.go @@ -626,6 +626,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if config.EnableConfigDiscovery { req.EnableConfigDiscovery = Bool(true) } + req.EnableOnDemandInstructionDiscovery = config.EnableOnDemandInstructionDiscovery req.Tools = config.Tools wireSystemMessage, transformCallbacks := extractTransformCallbacks(config.SystemMessage) req.SystemMessage = wireSystemMessage @@ -834,6 +835,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, if config.EnableConfigDiscovery { req.EnableConfigDiscovery = Bool(true) } + req.EnableOnDemandInstructionDiscovery = config.EnableOnDemandInstructionDiscovery if config.DisableResume { req.DisableResume = Bool(true) } diff --git a/go/client_test.go b/go/client_test.go index 42e45ea15..41b555411 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -1321,6 +1321,100 @@ func TestResumeSessionRequest_IncludeSubAgentStreamingEvents(t *testing.T) { }) } +func TestCreateSessionRequest_EnableOnDemandInstructionDiscovery(t *testing.T) { + t.Run("forwards explicit true", func(t *testing.T) { + req := createSessionRequest{ + EnableOnDemandInstructionDiscovery: 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["enableOnDemandInstructionDiscovery"] != true { + t.Errorf("Expected enableOnDemandInstructionDiscovery to be true, got %v", m["enableOnDemandInstructionDiscovery"]) + } + }) + + t.Run("preserves explicit false", func(t *testing.T) { + req := createSessionRequest{ + EnableOnDemandInstructionDiscovery: 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["enableOnDemandInstructionDiscovery"] != false { + t.Errorf("Expected enableOnDemandInstructionDiscovery to be false, got %v", m["enableOnDemandInstructionDiscovery"]) + } + }) + + t.Run("omits enableOnDemandInstructionDiscovery when not set", func(t *testing.T) { + req := createSessionRequest{} + data, _ := json.Marshal(req) + var m map[string]any + json.Unmarshal(data, &m) + if _, ok := m["enableOnDemandInstructionDiscovery"]; ok { + t.Error("Expected enableOnDemandInstructionDiscovery to be omitted when not set") + } + }) +} + +func TestResumeSessionRequest_EnableOnDemandInstructionDiscovery(t *testing.T) { + t.Run("forwards explicit true", func(t *testing.T) { + req := resumeSessionRequest{ + SessionID: "s1", + EnableOnDemandInstructionDiscovery: 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["enableOnDemandInstructionDiscovery"] != true { + t.Errorf("Expected enableOnDemandInstructionDiscovery to be true, got %v", m["enableOnDemandInstructionDiscovery"]) + } + }) + + t.Run("preserves explicit false", func(t *testing.T) { + req := resumeSessionRequest{ + SessionID: "s1", + EnableOnDemandInstructionDiscovery: 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["enableOnDemandInstructionDiscovery"] != false { + t.Errorf("Expected enableOnDemandInstructionDiscovery to be false, got %v", m["enableOnDemandInstructionDiscovery"]) + } + }) + + t.Run("omits enableOnDemandInstructionDiscovery when not set", func(t *testing.T) { + req := resumeSessionRequest{SessionID: "s1"} + data, _ := json.Marshal(req) + var m map[string]any + json.Unmarshal(data, &m) + if _, ok := m["enableOnDemandInstructionDiscovery"]; ok { + t.Error("Expected enableOnDemandInstructionDiscovery to be omitted when not set") + } + }) +} + func TestCreateSessionResponse_Capabilities(t *testing.T) { t.Run("reads capabilities from session.create response", func(t *testing.T) { responseJSON := `{"sessionId":"s1","workspacePath":"/tmp","capabilities":{"ui":{"elicitation":true}}}` diff --git a/go/internal/e2e/client_options_e2e_test.go b/go/internal/e2e/client_options_e2e_test.go index f490e99d6..251d81e1e 100644 --- a/go/internal/e2e/client_options_e2e_test.go +++ b/go/internal/e2e/client_options_e2e_test.go @@ -198,9 +198,10 @@ func TestClientOptionsE2E(t *testing.T) { } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - EnableConfigDiscovery: true, - IncludeSubAgentStreamingEvents: copilot.Bool(false), - OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + EnableConfigDiscovery: true, + EnableOnDemandInstructionDiscovery: copilot.Bool(true), + IncludeSubAgentStreamingEvents: copilot.Bool(false), + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { t.Fatalf("CreateSession failed: %v", err) @@ -225,6 +226,9 @@ func TestClientOptionsE2E(t *testing.T) { if v, ok := params["enableConfigDiscovery"].(bool); !ok || v != true { t.Errorf("Expected session.create.params.enableConfigDiscovery=true, got %v", params["enableConfigDiscovery"]) } + if v, ok := params["enableOnDemandInstructionDiscovery"].(bool); !ok || v != true { + t.Errorf("Expected session.create.params.enableOnDemandInstructionDiscovery=true, got %v", params["enableOnDemandInstructionDiscovery"]) + } if v, ok := params["includeSubAgentStreamingEvents"].(bool); !ok || v != false { t.Errorf("Expected session.create.params.includeSubAgentStreamingEvents=false, got %v", params["includeSubAgentStreamingEvents"]) } diff --git a/go/types.go b/go/types.go index 68a1c38a3..b5bc2ef67 100644 --- a/go/types.go +++ b/go/types.go @@ -613,6 +613,18 @@ type SessionConfig struct { // Custom instruction files (.github/copilot-instructions.md, AGENTS.md, etc.) are // always loaded from the working directory regardless of this setting. EnableConfigDiscovery bool + // EnableOnDemandInstructionDiscovery, when set, requests on-demand discovery + // of custom instruction files after the agent successfully reads or views + // files. Discovered instruction files are treated as model instructions and + // may influence agent behavior. Runtime-gated: only takes effect when custom + // instructions are enabled and the connected runtime supports and enables + // on-demand custom instruction discovery. Enable only for trusted + // repositories or workspaces; discovered instruction files may be stored or + // replayed with session history. When nil (the default), the option is + // omitted from the wire payload and the runtime treats the setting as off. + // For resumed sessions, omitting this option leaves the existing session + // setting unchanged; pass Bool(false) to disable future on-demand discovery. + EnableOnDemandInstructionDiscovery *bool // Tools exposes caller-implemented tools to the CLI Tools []Tool // SystemMessage configures system message customization @@ -889,6 +901,12 @@ type ResumeSessionConfig struct { // Custom instruction files (.github/copilot-instructions.md, AGENTS.md, etc.) are // always loaded from the working directory regardless of this setting. EnableConfigDiscovery bool + // EnableOnDemandInstructionDiscovery, when set, requests on-demand discovery + // of custom instruction files after the agent successfully reads or views + // files. See SessionConfig.EnableOnDemandInstructionDiscovery for details. + // For resumed sessions, omitting this option leaves the existing session + // setting unchanged; pass Bool(false) to disable future on-demand discovery. + EnableOnDemandInstructionDiscovery *bool // Streaming enables streaming of assistant message and reasoning chunks. // When true, assistant.message_delta and assistant.reasoning_delta events // with deltaContent are sent as the response is generated. @@ -1142,43 +1160,44 @@ type SessionLifecycleHandler func(event SessionLifecycleEvent) // createSessionRequest is the request for session.create type createSessionRequest struct { - Model string `json:"model,omitempty"` - SessionID string `json:"sessionId,omitempty"` - ClientName string `json:"clientName,omitempty"` - ReasoningEffort string `json:"reasoningEffort,omitempty"` - Tools []Tool `json:"tools,omitempty"` - SystemMessage *SystemMessageConfig `json:"systemMessage,omitempty"` - AvailableTools []string `json:"availableTools"` - ExcludedTools []string `json:"excludedTools,omitempty"` - Provider *ProviderConfig `json:"provider,omitempty"` - EnableSessionTelemetry *bool `json:"enableSessionTelemetry,omitempty"` - ModelCapabilities *rpc.ModelCapabilitiesOverride `json:"modelCapabilities,omitempty"` - RequestPermission *bool `json:"requestPermission,omitempty"` - RequestUserInput *bool `json:"requestUserInput,omitempty"` - RequestExitPlanMode *bool `json:"requestExitPlanMode,omitempty"` - RequestAutoModeSwitch *bool `json:"requestAutoModeSwitch,omitempty"` - Hooks *bool `json:"hooks,omitempty"` - WorkingDirectory string `json:"workingDirectory,omitempty"` - Streaming *bool `json:"streaming,omitempty"` - IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"` - MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"` - EnvValueMode string `json:"envValueMode,omitempty"` - CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"` - DefaultAgent *DefaultAgentConfig `json:"defaultAgent,omitempty"` - Agent string `json:"agent,omitempty"` - ConfigDir string `json:"configDir,omitempty"` - EnableConfigDiscovery *bool `json:"enableConfigDiscovery,omitempty"` - SkillDirectories []string `json:"skillDirectories,omitempty"` - InstructionDirectories []string `json:"instructionDirectories,omitempty"` - DisabledSkills []string `json:"disabledSkills,omitempty"` - InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` - Commands []wireCommand `json:"commands,omitempty"` - RequestElicitation *bool `json:"requestElicitation,omitempty"` - GitHubToken string `json:"gitHubToken,omitempty"` - RemoteSession rpc.RemoteSessionMode `json:"remoteSession,omitempty"` - Cloud *CloudSessionOptions `json:"cloud,omitempty"` - Traceparent string `json:"traceparent,omitempty"` - Tracestate string `json:"tracestate,omitempty"` + Model string `json:"model,omitempty"` + SessionID string `json:"sessionId,omitempty"` + ClientName string `json:"clientName,omitempty"` + ReasoningEffort string `json:"reasoningEffort,omitempty"` + Tools []Tool `json:"tools,omitempty"` + SystemMessage *SystemMessageConfig `json:"systemMessage,omitempty"` + AvailableTools []string `json:"availableTools"` + ExcludedTools []string `json:"excludedTools,omitempty"` + Provider *ProviderConfig `json:"provider,omitempty"` + EnableSessionTelemetry *bool `json:"enableSessionTelemetry,omitempty"` + ModelCapabilities *rpc.ModelCapabilitiesOverride `json:"modelCapabilities,omitempty"` + RequestPermission *bool `json:"requestPermission,omitempty"` + RequestUserInput *bool `json:"requestUserInput,omitempty"` + RequestExitPlanMode *bool `json:"requestExitPlanMode,omitempty"` + RequestAutoModeSwitch *bool `json:"requestAutoModeSwitch,omitempty"` + Hooks *bool `json:"hooks,omitempty"` + WorkingDirectory string `json:"workingDirectory,omitempty"` + Streaming *bool `json:"streaming,omitempty"` + IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"` + MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"` + EnvValueMode string `json:"envValueMode,omitempty"` + CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"` + DefaultAgent *DefaultAgentConfig `json:"defaultAgent,omitempty"` + Agent string `json:"agent,omitempty"` + ConfigDir string `json:"configDir,omitempty"` + EnableConfigDiscovery *bool `json:"enableConfigDiscovery,omitempty"` + EnableOnDemandInstructionDiscovery *bool `json:"enableOnDemandInstructionDiscovery,omitempty"` + SkillDirectories []string `json:"skillDirectories,omitempty"` + InstructionDirectories []string `json:"instructionDirectories,omitempty"` + DisabledSkills []string `json:"disabledSkills,omitempty"` + InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` + Commands []wireCommand `json:"commands,omitempty"` + RequestElicitation *bool `json:"requestElicitation,omitempty"` + GitHubToken string `json:"gitHubToken,omitempty"` + RemoteSession rpc.RemoteSessionMode `json:"remoteSession,omitempty"` + Cloud *CloudSessionOptions `json:"cloud,omitempty"` + Traceparent string `json:"traceparent,omitempty"` + Tracestate string `json:"tracestate,omitempty"` } // wireCommand is the wire representation of a command (name + description only, no handler). @@ -1196,44 +1215,45 @@ type createSessionResponse struct { // resumeSessionRequest is the request for session.resume type resumeSessionRequest struct { - SessionID string `json:"sessionId"` - ClientName string `json:"clientName,omitempty"` - Model string `json:"model,omitempty"` - ReasoningEffort string `json:"reasoningEffort,omitempty"` - Tools []Tool `json:"tools,omitempty"` - SystemMessage *SystemMessageConfig `json:"systemMessage,omitempty"` - AvailableTools []string `json:"availableTools"` - ExcludedTools []string `json:"excludedTools,omitempty"` - Provider *ProviderConfig `json:"provider,omitempty"` - EnableSessionTelemetry *bool `json:"enableSessionTelemetry,omitempty"` - ModelCapabilities *rpc.ModelCapabilitiesOverride `json:"modelCapabilities,omitempty"` - RequestPermission *bool `json:"requestPermission,omitempty"` - RequestUserInput *bool `json:"requestUserInput,omitempty"` - RequestExitPlanMode *bool `json:"requestExitPlanMode,omitempty"` - RequestAutoModeSwitch *bool `json:"requestAutoModeSwitch,omitempty"` - Hooks *bool `json:"hooks,omitempty"` - WorkingDirectory string `json:"workingDirectory,omitempty"` - ConfigDir string `json:"configDir,omitempty"` - EnableConfigDiscovery *bool `json:"enableConfigDiscovery,omitempty"` - DisableResume *bool `json:"disableResume,omitempty"` - ContinuePendingWork *bool `json:"continuePendingWork,omitempty"` - Streaming *bool `json:"streaming,omitempty"` - IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"` - MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"` - EnvValueMode string `json:"envValueMode,omitempty"` - CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"` - DefaultAgent *DefaultAgentConfig `json:"defaultAgent,omitempty"` - Agent string `json:"agent,omitempty"` - SkillDirectories []string `json:"skillDirectories,omitempty"` - InstructionDirectories []string `json:"instructionDirectories,omitempty"` - DisabledSkills []string `json:"disabledSkills,omitempty"` - InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` - Commands []wireCommand `json:"commands,omitempty"` - RequestElicitation *bool `json:"requestElicitation,omitempty"` - GitHubToken string `json:"gitHubToken,omitempty"` - RemoteSession rpc.RemoteSessionMode `json:"remoteSession,omitempty"` - Traceparent string `json:"traceparent,omitempty"` - Tracestate string `json:"tracestate,omitempty"` + SessionID string `json:"sessionId"` + ClientName string `json:"clientName,omitempty"` + Model string `json:"model,omitempty"` + ReasoningEffort string `json:"reasoningEffort,omitempty"` + Tools []Tool `json:"tools,omitempty"` + SystemMessage *SystemMessageConfig `json:"systemMessage,omitempty"` + AvailableTools []string `json:"availableTools"` + ExcludedTools []string `json:"excludedTools,omitempty"` + Provider *ProviderConfig `json:"provider,omitempty"` + EnableSessionTelemetry *bool `json:"enableSessionTelemetry,omitempty"` + ModelCapabilities *rpc.ModelCapabilitiesOverride `json:"modelCapabilities,omitempty"` + RequestPermission *bool `json:"requestPermission,omitempty"` + RequestUserInput *bool `json:"requestUserInput,omitempty"` + RequestExitPlanMode *bool `json:"requestExitPlanMode,omitempty"` + RequestAutoModeSwitch *bool `json:"requestAutoModeSwitch,omitempty"` + Hooks *bool `json:"hooks,omitempty"` + WorkingDirectory string `json:"workingDirectory,omitempty"` + ConfigDir string `json:"configDir,omitempty"` + EnableConfigDiscovery *bool `json:"enableConfigDiscovery,omitempty"` + EnableOnDemandInstructionDiscovery *bool `json:"enableOnDemandInstructionDiscovery,omitempty"` + DisableResume *bool `json:"disableResume,omitempty"` + ContinuePendingWork *bool `json:"continuePendingWork,omitempty"` + Streaming *bool `json:"streaming,omitempty"` + IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"` + MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"` + EnvValueMode string `json:"envValueMode,omitempty"` + CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"` + DefaultAgent *DefaultAgentConfig `json:"defaultAgent,omitempty"` + Agent string `json:"agent,omitempty"` + SkillDirectories []string `json:"skillDirectories,omitempty"` + InstructionDirectories []string `json:"instructionDirectories,omitempty"` + DisabledSkills []string `json:"disabledSkills,omitempty"` + InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` + Commands []wireCommand `json:"commands,omitempty"` + RequestElicitation *bool `json:"requestElicitation,omitempty"` + GitHubToken string `json:"gitHubToken,omitempty"` + RemoteSession rpc.RemoteSessionMode `json:"remoteSession,omitempty"` + Traceparent string `json:"traceparent,omitempty"` + Tracestate string `json:"tracestate,omitempty"` } // resumeSessionResponse is the response from session.resume diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 1f0e8e9c9..5f997723c 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -830,6 +830,7 @@ export class CopilotClient { agent: config.agent, configDir: config.configDir, enableConfigDiscovery: config.enableConfigDiscovery, + enableOnDemandInstructionDiscovery: config.enableOnDemandInstructionDiscovery, skillDirectories: config.skillDirectories, instructionDirectories: config.instructionDirectories, disabledSkills: config.disabledSkills, @@ -978,6 +979,7 @@ export class CopilotClient { workingDirectory: config.workingDirectory, configDir: config.configDir, enableConfigDiscovery: config.enableConfigDiscovery, + enableOnDemandInstructionDiscovery: config.enableOnDemandInstructionDiscovery, streaming: config.streaming, includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true, mcpServers: config.mcpServers, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 0cdf84ad3..218fd2688 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1334,6 +1334,28 @@ export interface SessionConfig { */ enableConfigDiscovery?: boolean; + /** + * When true, requests on-demand discovery of custom instruction files after + * the agent successfully reads or views files. Discovered instruction files + * are treated as model instructions and may influence agent behavior. + * + * Runtime-gated: this only takes effect when custom instructions are enabled + * and the connected runtime supports and enables on-demand custom instruction + * discovery. Otherwise the runtime accepts the option but performs no + * on-demand instruction discovery. + * + * Security: enable only for trusted repositories or workspaces. Discovered + * instruction files may be stored or replayed with session history. Do not + * enable for untrusted content, CI jobs processing untrusted forks, or + * directories writable by untrusted users or processes. + * + * For resumed sessions, omitting this option leaves the existing session + * setting unchanged; pass `false` to disable future on-demand discovery. + * + * @default false + */ + enableOnDemandInstructionDiscovery?: boolean; + /** * Tools exposed to the CLI server */ @@ -1563,6 +1585,7 @@ export type ResumeSessionConfig = Pick< | "workingDirectory" | "configDir" | "enableConfigDiscovery" + | "enableOnDemandInstructionDiscovery" | "mcpServers" | "customAgents" | "defaultAgent" diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index c3090eb76..f3a913357 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -164,6 +164,50 @@ describe("CopilotClient", () => { spy.mockRestore(); }); + it("forwards enableOnDemandInstructionDiscovery in session.create request", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi.spyOn((client as any).connection!, "sendRequest"); + await client.createSession({ + enableOnDemandInstructionDiscovery: false, + onPermissionRequest: approveAll, + }); + + expect(spy).toHaveBeenCalledWith( + "session.create", + expect.objectContaining({ enableOnDemandInstructionDiscovery: false }) + ); + }); + + it("forwards enableOnDemandInstructionDiscovery in session.resume request", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + await client.resumeSession(session.sessionId, { + enableOnDemandInstructionDiscovery: false, + onPermissionRequest: approveAll, + }); + + expect(spy).toHaveBeenCalledWith( + "session.resume", + expect.objectContaining({ + enableOnDemandInstructionDiscovery: false, + sessionId: session.sessionId, + }) + ); + spy.mockRestore(); + }); + it("defaults includeSubAgentStreamingEvents to true in session.create when not specified", async () => { const client = new CopilotClient(); await client.start(); diff --git a/nodejs/test/e2e/client_options.e2e.test.ts b/nodejs/test/e2e/client_options.e2e.test.ts index d67b6a243..687ba01f6 100644 --- a/nodejs/test/e2e/client_options.e2e.test.ts +++ b/nodejs/test/e2e/client_options.e2e.test.ts @@ -300,6 +300,7 @@ describe("Client options", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, enableConfigDiscovery: true, + enableOnDemandInstructionDiscovery: true, includeSubAgentStreamingEvents: false, }); @@ -309,6 +310,7 @@ describe("Client options", async () => { method: string; params: { enableConfigDiscovery?: boolean; + enableOnDemandInstructionDiscovery?: boolean; includeSubAgentStreamingEvents?: boolean; }; }[]; @@ -316,6 +318,7 @@ describe("Client options", async () => { const createRequests = updated.requests.filter((r) => r.method === "session.create"); expect(createRequests).toHaveLength(1); expect(createRequests[0].params.enableConfigDiscovery).toBe(true); + expect(createRequests[0].params.enableOnDemandInstructionDiscovery).toBe(true); expect(createRequests[0].params.includeSubAgentStreamingEvents).toBe(false); await session.disconnect(); diff --git a/python/copilot/client.py b/python/copilot/client.py index e7acd2c25..a8974ca27 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -1345,6 +1345,7 @@ async def create_session( agent: str | None = None, config_dir: str | None = None, enable_config_discovery: bool | None = None, + enable_on_demand_instruction_discovery: bool | None = None, skill_directories: list[str] | None = None, instruction_directories: list[str] | None = None, disabled_skills: list[str] | None = None, @@ -1414,6 +1415,17 @@ async def create_session( explicit values taking precedence on name collision. Custom instruction files (``.github/copilot-instructions.md``, ``AGENTS.md``, etc.) are always loaded regardless of this setting. + enable_on_demand_instruction_discovery: When True, requests on-demand + discovery of custom instruction files after the agent successfully + reads or views files. Discovered instruction files are treated as + model instructions and may influence agent behavior. Runtime-gated: + only takes effect when custom instructions are enabled and the + connected runtime supports and enables on-demand custom instruction + discovery. Enable only for trusted repositories or workspaces; + discovered instruction files may be stored or replayed with session + history. For resumed sessions, omitting this option leaves the + existing session setting unchanged; pass False to disable future + on-demand discovery. skill_directories: Directories to search for skills. instruction_directories: Additional directories to search for custom instruction files. @@ -1575,6 +1587,10 @@ async def create_session( if enable_config_discovery is not None: payload["enableConfigDiscovery"] = enable_config_discovery + # Add on-demand instruction discovery flag if provided + if enable_on_demand_instruction_discovery is not None: + payload["enableOnDemandInstructionDiscovery"] = enable_on_demand_instruction_discovery + # Add skill directories configuration if provided if skill_directories: payload["skillDirectories"] = skill_directories @@ -1718,6 +1734,7 @@ async def resume_session( agent: str | None = None, config_dir: str | None = None, enable_config_discovery: bool | None = None, + enable_on_demand_instruction_discovery: bool | None = None, skill_directories: list[str] | None = None, instruction_directories: list[str] | None = None, disabled_skills: list[str] | None = None, @@ -1787,6 +1804,17 @@ async def resume_session( explicit values taking precedence on name collision. Custom instruction files (``.github/copilot-instructions.md``, ``AGENTS.md``, etc.) are always loaded regardless of this setting. + enable_on_demand_instruction_discovery: When True, requests on-demand + discovery of custom instruction files after the agent successfully + reads or views files. Discovered instruction files are treated as + model instructions and may influence agent behavior. Runtime-gated: + only takes effect when custom instructions are enabled and the + connected runtime supports and enables on-demand custom instruction + discovery. Enable only for trusted repositories or workspaces; + discovered instruction files may be stored or replayed with session + history. For resumed sessions, omitting this option leaves the + existing session setting unchanged; pass False to disable future + on-demand discovery. skill_directories: Directories to search for skills. instruction_directories: Additional directories to search for custom instruction files. @@ -1911,6 +1939,8 @@ async def resume_session( payload["configDir"] = config_dir if enable_config_discovery is not None: payload["enableConfigDiscovery"] = enable_config_discovery + if enable_on_demand_instruction_discovery is not None: + payload["enableOnDemandInstructionDiscovery"] = enable_on_demand_instruction_discovery if continue_pending_work is not None: payload["continuePendingWork"] = continue_pending_work diff --git a/python/e2e/test_client_options_e2e.py b/python/e2e/test_client_options_e2e.py index 7992524d1..eec05f1d8 100644 --- a/python/e2e/test_client_options_e2e.py +++ b/python/e2e/test_client_options_e2e.py @@ -260,6 +260,7 @@ async def test_should_propagate_process_options_to_spawned_cli(self, ctx: E2ETes session = await client.create_session( on_permission_request=PermissionHandler.approve_all, enable_config_discovery=True, + enable_on_demand_instruction_discovery=True, include_sub_agent_streaming_events=False, ) try: @@ -270,6 +271,7 @@ async def test_should_propagate_process_options_to_spawned_cli(self, ctx: E2ETes ) params = create_request["params"] assert params["enableConfigDiscovery"] is True + assert params["enableOnDemandInstructionDiscovery"] is True assert params["includeSubAgentStreamingEvents"] is False finally: await session.disconnect() diff --git a/python/test_client.py b/python/test_client.py index f7c2e3bf0..f7f97579d 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -633,6 +633,57 @@ async def mock_request(method, params): finally: await client.force_stop() + @pytest.mark.asyncio + async def test_create_session_forwards_enable_on_demand_instruction_discovery(self): + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + await client.start() + + try: + captured = {} + original_request = client._client.request + + async def mock_request(method, params): + captured[method] = params + return await original_request(method, params) + + client._client.request = mock_request + await client.create_session( + on_permission_request=PermissionHandler.approve_all, + enable_on_demand_instruction_discovery=False, + ) + assert captured["session.create"]["enableOnDemandInstructionDiscovery"] is False + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_resume_session_forwards_enable_on_demand_instruction_discovery(self): + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + await client.start() + + try: + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) + + captured = {} + original_request = client._client.request + + async def mock_request(method, params): + captured[method] = params + if method == "session.resume": + return {"sessionId": session.session_id} + return await original_request(method, params) + + client._client.request = mock_request + await client.resume_session( + session.session_id, + on_permission_request=PermissionHandler.approve_all, + enable_on_demand_instruction_discovery=False, + ) + assert captured["session.resume"]["enableOnDemandInstructionDiscovery"] is False + finally: + await client.force_stop() + @pytest.mark.asyncio async def test_create_session_forwards_provider_headers(self): client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) diff --git a/rust/src/types.rs b/rust/src/types.rs index 2858f3c50..7978aa8e1 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -1052,6 +1052,23 @@ pub struct SessionConfig { /// When true, the CLI runs config discovery (MCP config files, skills, plugins). #[serde(skip_serializing_if = "Option::is_none")] pub enable_config_discovery: Option, + /// When `Some(true)`, requests on-demand discovery of custom instruction + /// files after the agent successfully reads or views files. Discovered + /// instruction files are treated as model instructions and may influence + /// agent behavior. + /// + /// Runtime-gated: only takes effect when custom instructions are enabled + /// and the connected runtime supports and enables on-demand custom + /// instruction discovery. Otherwise the runtime accepts the option but + /// performs no on-demand instruction discovery. + /// + /// Security: enable only for trusted repositories or workspaces. + /// Discovered instruction files may be stored or replayed with session + /// history. Do not enable for untrusted content, CI jobs processing + /// untrusted forks, or directories writable by untrusted users or + /// processes. + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_on_demand_instruction_discovery: Option, /// Enable the `ask_user` tool for interactive user input. Defaults to /// `Some(true)` via [`SessionConfig::default`]. #[serde(skip_serializing_if = "Option::is_none")] @@ -1203,6 +1220,10 @@ impl std::fmt::Debug for SessionConfig { .field("excluded_tools", &self.excluded_tools) .field("mcp_servers", &self.mcp_servers) .field("enable_config_discovery", &self.enable_config_discovery) + .field( + "enable_on_demand_instruction_discovery", + &self.enable_on_demand_instruction_discovery, + ) .field("request_user_input", &self.request_user_input) .field("request_permission", &self.request_permission) .field("request_exit_plan_mode", &self.request_exit_plan_mode) @@ -1267,6 +1288,7 @@ impl Default for SessionConfig { mcp_servers: None, env_value_mode: default_env_value_mode(), enable_config_discovery: None, + enable_on_demand_instruction_discovery: None, request_user_input: Some(true), request_permission: Some(true), request_exit_plan_mode: Some(true), @@ -1463,6 +1485,14 @@ impl SessionConfig { self } + /// Enable or disable on-demand discovery of custom instruction files. + /// See [`Self::enable_on_demand_instruction_discovery`] for details, + /// runtime gating, and security considerations. + pub fn with_enable_on_demand_instruction_discovery(mut self, enable: bool) -> Self { + self.enable_on_demand_instruction_discovery = Some(enable); + self + } + /// Enable the `ask_user` tool. Defaults to `Some(true)` via [`Self::default`]. pub fn with_request_user_input(mut self, enable: bool) -> Self { self.request_user_input = Some(enable); @@ -1664,6 +1694,15 @@ pub struct ResumeSessionConfig { /// Enable config discovery on resume. #[serde(skip_serializing_if = "Option::is_none")] pub enable_config_discovery: Option, + /// When `Some(true)`, requests on-demand discovery of custom instruction + /// files after the agent successfully reads or views files on resume. + /// See [`SessionConfig::enable_on_demand_instruction_discovery`] for + /// runtime gating and security details. + /// + /// On resume, omitting this option leaves the existing session setting + /// unchanged; pass `Some(false)` to disable future on-demand discovery. + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_on_demand_instruction_discovery: Option, /// Enable the ask_user tool. #[serde(skip_serializing_if = "Option::is_none")] pub request_user_input: Option, @@ -1786,6 +1825,10 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("excluded_tools", &self.excluded_tools) .field("mcp_servers", &self.mcp_servers) .field("enable_config_discovery", &self.enable_config_discovery) + .field( + "enable_on_demand_instruction_discovery", + &self.enable_on_demand_instruction_discovery, + ) .field("request_user_input", &self.request_user_input) .field("request_permission", &self.request_permission) .field("request_exit_plan_mode", &self.request_exit_plan_mode) @@ -1848,6 +1891,7 @@ impl ResumeSessionConfig { mcp_servers: None, env_value_mode: default_env_value_mode(), enable_config_discovery: None, + enable_on_demand_instruction_discovery: None, request_user_input: Some(true), request_permission: Some(true), request_exit_plan_mode: Some(true), @@ -2014,6 +2058,16 @@ impl ResumeSessionConfig { self } + /// Enable or disable on-demand discovery of custom instruction files on + /// resume. See + /// [`SessionConfig::with_enable_on_demand_instruction_discovery`] for + /// details. On resume, omitting this leaves the existing session setting + /// unchanged; pass `false` to disable future on-demand discovery. + pub fn with_enable_on_demand_instruction_discovery(mut self, enable: bool) -> Self { + self.enable_on_demand_instruction_discovery = Some(enable); + self + } + /// Enable the `ask_user` tool. Defaults to `Some(true)` via [`Self::new`]. pub fn with_request_user_input(mut self, enable: bool) -> Self { self.request_user_input = Some(enable); @@ -3394,6 +3448,7 @@ mod tests { .with_excluded_tools(["dangerous"]) .with_mcp_servers(HashMap::new()) .with_enable_config_discovery(true) + .with_enable_on_demand_instruction_discovery(true) .with_request_user_input(false) .with_request_exit_plan_mode(false) .with_request_auto_mode_switch(false) @@ -3422,6 +3477,7 @@ mod tests { ); assert!(cfg.mcp_servers.is_some()); assert_eq!(cfg.enable_config_discovery, Some(true)); + assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true)); assert_eq!(cfg.request_user_input, Some(false)); // overrode default assert_eq!(cfg.request_permission, Some(true)); // default preserved assert_eq!(cfg.request_exit_plan_mode, Some(false)); @@ -3454,6 +3510,7 @@ mod tests { .with_excluded_tools(["dangerous"]) .with_mcp_servers(HashMap::new()) .with_enable_config_discovery(true) + .with_enable_on_demand_instruction_discovery(false) .with_request_user_input(false) .with_request_exit_plan_mode(false) .with_request_auto_mode_switch(false) @@ -3482,6 +3539,7 @@ mod tests { ); assert!(cfg.mcp_servers.is_some()); assert_eq!(cfg.enable_config_discovery, Some(true)); + assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false)); assert_eq!(cfg.request_user_input, Some(false)); // overrode default assert_eq!(cfg.request_permission, Some(true)); // default preserved assert_eq!(cfg.request_exit_plan_mode, Some(false)); diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 81ddf54f5..709884f55 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -2665,6 +2665,47 @@ fn resume_session_config_serializes_bucket_b_fields() { assert!(!debug.contains("ghs_secret"), "leaked token: {debug}"); } +#[test] +fn session_config_serializes_enable_on_demand_instruction_discovery() { + use github_copilot_sdk::SessionConfig; + + let mut cfg_true = SessionConfig::default(); + cfg_true.enable_on_demand_instruction_discovery = Some(true); + let json_true = serde_json::to_value(&cfg_true).unwrap(); + assert_eq!(json_true["enableOnDemandInstructionDiscovery"], true); + + let mut cfg_false = SessionConfig::default(); + cfg_false.enable_on_demand_instruction_discovery = Some(false); + let json_false = serde_json::to_value(&cfg_false).unwrap(); + assert_eq!(json_false["enableOnDemandInstructionDiscovery"], false); + + let empty = serde_json::to_value(SessionConfig::default()).unwrap(); + assert!(empty.get("enableOnDemandInstructionDiscovery").is_none()); +} + +#[test] +fn resume_session_config_serializes_enable_on_demand_instruction_discovery() { + use github_copilot_sdk::{ResumeSessionConfig, SessionId}; + + let mut cfg_true = ResumeSessionConfig::new(SessionId::from("sess-true")); + cfg_true.enable_on_demand_instruction_discovery = Some(true); + let json_true = serde_json::to_value(&cfg_true).unwrap(); + assert_eq!(json_true["enableOnDemandInstructionDiscovery"], true); + + let mut cfg_false = ResumeSessionConfig::new(SessionId::from("sess-false")); + cfg_false.enable_on_demand_instruction_discovery = Some(false); + let json_false = serde_json::to_value(&cfg_false).unwrap(); + assert_eq!(json_false["enableOnDemandInstructionDiscovery"], false); + + let empty = ResumeSessionConfig::new(SessionId::from("sess-omitted")); + let empty_json = serde_json::to_value(&empty).unwrap(); + assert!( + empty_json + .get("enableOnDemandInstructionDiscovery") + .is_none() + ); +} + // ===================================================================== // Slash commands (ยง 4.1) // =====================================================================