From 07bfd0a3b4bb0e0f15908b389631a05e74b43396 Mon Sep 17 00:00:00 2001 From: Tomas Meszaros Date: Mon, 18 May 2026 16:55:04 +0000 Subject: [PATCH] Add enableOnDemandInstructionDiscovery to all SDK SessionConfig types Mirrors the existing enableConfigDiscovery and remoteSession precedents (PRs #1044 and #1295). Exposes the SDK option that lets the runtime discover custom instruction files on demand after the agent reads or views files, complementing the existing up-front load of `.github/copilot-instructions.md`, `AGENTS.md`, etc. Wire key (camelCase, identical across all 5 SDKs): enableOnDemandInstructionDiscovery Type shapes: Node enableOnDemandInstructionDiscovery?: boolean Python enable_on_demand_instruction_discovery: bool | None = None Go EnableOnDemandInstructionDiscovery *bool .NET bool? EnableOnDemandInstructionDiscovery Rust Option with #[serde(skip_serializing_if = "Option::is_none")] Wire semantics: when set, the wire payload carries the literal value (including explicit `false`); when omitted, the key is dropped. Applies to both session.create and session.resume so callers can toggle the setting on a resumed session. Runtime-gated. The runtime honors the option only when custom instructions are enabled and the connected runtime supports on-demand custom instruction discovery; otherwise the option is accepted but no-ops. The SDK does not attempt to detect the runtime gate. Requires @github/copilot ^1.0.49-1 (the runtime change shipped in github/copilot#7759). Security: discovered instruction files are treated as model instructions and may be stored or replayed with session history. Docstrings caution against enabling for untrusted content, CI jobs processing untrusted forks, or directories writable by untrusted users or processes. Go shape note: uses *bool (not bool) so consumers can disable a previously-enabled session on resume. Reuses the precedent already set by EnableSessionTelemetry *bool and IncludeSubAgentStreamingEvents *bool. Does not retrofit the existing EnableConfigDiscovery bool field (that would be a separate breaking source change). Tests: each SDK adds tests for the new field on both create and resume, asserting that explicit `false` is serialized as `false` and that omission drops the key from the payload. Mirrors the test patterns already in place for enable_session_telemetry, include_sub_agent_streaming_events, and enable_config_discovery. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Client.cs | 4 + dotnet/src/Types.cs | 32 ++++ dotnet/test/Unit/CloneTests.cs | 48 ++++++ dotnet/test/Unit/SerializationTests.cs | 54 +++++++ go/client.go | 2 + go/client_test.go | 94 ++++++++++++ go/internal/e2e/client_options_e2e_test.go | 10 +- go/types.go | 170 ++++++++++++--------- nodejs/src/client.ts | 2 + nodejs/src/types.ts | 23 +++ nodejs/test/client.test.ts | 44 ++++++ nodejs/test/e2e/client_options.e2e.test.ts | 3 + python/copilot/client.py | 30 ++++ python/e2e/test_client_options_e2e.py | 2 + python/test_client.py | 51 +++++++ rust/src/types.rs | 58 +++++++ rust/tests/session_test.rs | 41 +++++ 17 files changed, 590 insertions(+), 78 deletions(-) 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) // =====================================================================