Skip to content

Commit 07bfd0a

Browse files
Tomas MeszarosCopilot
andcommitted
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<bool> 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>
1 parent f6c1adf commit 07bfd0a

17 files changed

Lines changed: 590 additions & 78 deletions

dotnet/src/Client.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
622622
config.Agent,
623623
config.ConfigDir,
624624
config.EnableConfigDiscovery,
625+
config.EnableOnDemandInstructionDiscovery,
625626
config.SkillDirectories,
626627
config.DisabledSkills,
627628
config.InfiniteSessions,
@@ -776,6 +777,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
776777
config.WorkingDirectory,
777778
config.ConfigDir,
778779
config.EnableConfigDiscovery,
780+
config.EnableOnDemandInstructionDiscovery,
779781
config.DisableResume is true ? true : null,
780782
config.Streaming is true ? true : null,
781783
config.IncludeSubAgentStreamingEvents,
@@ -1992,6 +1994,7 @@ internal record CreateSessionRequest(
19921994
string? Agent,
19931995
string? ConfigDir,
19941996
bool? EnableConfigDiscovery,
1997+
bool? EnableOnDemandInstructionDiscovery,
19951998
IList<string>? SkillDirectories,
19961999
IList<string>? DisabledSkills,
19972000
InfiniteSessionConfig? InfiniteSessions,
@@ -2046,6 +2049,7 @@ internal record ResumeSessionRequest(
20462049
string? WorkingDirectory,
20472050
string? ConfigDir,
20482051
bool? EnableConfigDiscovery,
2052+
bool? EnableOnDemandInstructionDiscovery,
20492053
bool? DisableResume,
20502054
bool? Streaming,
20512055
bool? IncludeSubAgentStreamingEvents,

dotnet/src/Types.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2077,6 +2077,7 @@ protected SessionConfig(SessionConfig? other)
20772077
Agent = other.Agent;
20782078
DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
20792079
EnableConfigDiscovery = other.EnableConfigDiscovery;
2080+
EnableOnDemandInstructionDiscovery = other.EnableOnDemandInstructionDiscovery;
20802081
ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null;
20812082
Hooks = other.Hooks;
20822083
InfiniteSessions = other.InfiniteSessions;
@@ -2157,6 +2158,25 @@ protected SessionConfig(SessionConfig? other)
21572158
/// </summary>
21582159
public bool? EnableConfigDiscovery { get; set; }
21592160

2161+
/// <summary>
2162+
/// When <see langword="true"/>, requests on-demand discovery of custom instruction
2163+
/// files after the agent successfully reads or views files. Discovered instruction
2164+
/// files are treated as model instructions and may influence agent behavior.
2165+
/// <para>
2166+
/// Runtime-gated: only takes effect when custom instructions are enabled and the
2167+
/// connected runtime supports and enables on-demand custom instruction discovery.
2168+
/// Otherwise the runtime accepts the option but performs no on-demand instruction
2169+
/// discovery.
2170+
/// </para>
2171+
/// <para>
2172+
/// Security: enable only for trusted repositories or workspaces. Discovered
2173+
/// instruction files may be stored or replayed with session history. Do not enable
2174+
/// for untrusted content, CI jobs processing untrusted forks, or directories
2175+
/// writable by untrusted users or processes.
2176+
/// </para>
2177+
/// </summary>
2178+
public bool? EnableOnDemandInstructionDiscovery { get; set; }
2179+
21602180
/// <summary>
21612181
/// Custom tool functions available to the language model during the session.
21622182
/// </summary>
@@ -2385,6 +2405,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
23852405
DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
23862406
DisableResume = other.DisableResume;
23872407
EnableConfigDiscovery = other.EnableConfigDiscovery;
2408+
EnableOnDemandInstructionDiscovery = other.EnableOnDemandInstructionDiscovery;
23882409
ContinuePendingWork = other.ContinuePendingWork;
23892410
ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null;
23902411
Hooks = other.Hooks;
@@ -2543,6 +2564,17 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
25432564
/// </summary>
25442565
public bool? EnableConfigDiscovery { get; set; }
25452566

2567+
/// <summary>
2568+
/// When <see langword="true"/>, requests on-demand discovery of custom instruction
2569+
/// files after the agent successfully reads or views files. See
2570+
/// <see cref="SessionConfig.EnableOnDemandInstructionDiscovery"/> for details.
2571+
/// <para>
2572+
/// For resumed sessions, omitting this option leaves the existing session setting
2573+
/// unchanged; set <see langword="false"/> to disable future on-demand discovery.
2574+
/// </para>
2575+
/// </summary>
2576+
public bool? EnableOnDemandInstructionDiscovery { get; set; }
2577+
25462578
/// <summary>
25472579
/// When true, the session.resume event is not emitted.
25482580
/// Default: false (resume event is emitted).

dotnet/test/Unit/CloneTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
9292
WorkingDirectory = "/workspace",
9393
Streaming = true,
9494
EnableSessionTelemetry = false,
95+
EnableOnDemandInstructionDiscovery = true,
9596
IncludeSubAgentStreamingEvents = false,
9697
McpServers = new Dictionary<string, McpServerConfig> { ["server1"] = new McpStdioServerConfig { Command = "echo" } },
9798
CustomAgents = [new CustomAgentConfig { Name = "agent1", Model = "claude-haiku-4.5" }],
@@ -125,6 +126,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
125126
Assert.Equal(original.WorkingDirectory, clone.WorkingDirectory);
126127
Assert.Equal(original.Streaming, clone.Streaming);
127128
Assert.Equal(original.EnableSessionTelemetry, clone.EnableSessionTelemetry);
129+
Assert.Equal(original.EnableOnDemandInstructionDiscovery, clone.EnableOnDemandInstructionDiscovery);
128130
Assert.Equal(original.IncludeSubAgentStreamingEvents, clone.IncludeSubAgentStreamingEvents);
129131
Assert.Equal(original.McpServers.Count, clone.McpServers!.Count);
130132
Assert.Equal(original.CustomAgents.Count, clone.CustomAgents!.Count);
@@ -403,4 +405,50 @@ public void ResumeSessionConfig_Clone_PreservesEnableSessionTelemetryDefault()
403405

404406
Assert.Null(clone.EnableSessionTelemetry);
405407
}
408+
409+
[Fact]
410+
public void SessionConfig_Clone_CopiesEnableOnDemandInstructionDiscovery()
411+
{
412+
var original = new SessionConfig
413+
{
414+
EnableOnDemandInstructionDiscovery = false,
415+
};
416+
417+
var clone = original.Clone();
418+
419+
Assert.False(clone.EnableOnDemandInstructionDiscovery);
420+
}
421+
422+
[Fact]
423+
public void ResumeSessionConfig_Clone_CopiesEnableOnDemandInstructionDiscovery()
424+
{
425+
var original = new ResumeSessionConfig
426+
{
427+
EnableOnDemandInstructionDiscovery = true,
428+
};
429+
430+
var clone = original.Clone();
431+
432+
Assert.True(clone.EnableOnDemandInstructionDiscovery);
433+
}
434+
435+
[Fact]
436+
public void SessionConfig_Clone_PreservesEnableOnDemandInstructionDiscoveryDefault()
437+
{
438+
var original = new SessionConfig();
439+
440+
var clone = original.Clone();
441+
442+
Assert.Null(clone.EnableOnDemandInstructionDiscovery);
443+
}
444+
445+
[Fact]
446+
public void ResumeSessionConfig_Clone_PreservesEnableOnDemandInstructionDiscoveryDefault()
447+
{
448+
var original = new ResumeSessionConfig();
449+
450+
var clone = original.Clone();
451+
452+
Assert.Null(clone.EnableOnDemandInstructionDiscovery);
453+
}
406454
}

dotnet/test/Unit/SerializationTests.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,60 @@ public void ResumeSessionRequest_CanSerializeEnableSessionTelemetry_WithSdkOptio
203203
Assert.False(root.GetProperty("enableSessionTelemetry").GetBoolean());
204204
}
205205

206+
[Fact]
207+
public void CreateSessionRequest_CanSerializeEnableOnDemandInstructionDiscovery_WithSdkOptions()
208+
{
209+
var options = GetSerializerOptions();
210+
var requestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest");
211+
212+
var requestTrue = CreateInternalRequest(
213+
requestType,
214+
("SessionId", "session-id"),
215+
("EnableOnDemandInstructionDiscovery", true));
216+
var rootTrue = JsonDocument.Parse(JsonSerializer.Serialize(requestTrue, requestType, options)).RootElement;
217+
Assert.True(rootTrue.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean());
218+
219+
var requestFalse = CreateInternalRequest(
220+
requestType,
221+
("SessionId", "session-id"),
222+
("EnableOnDemandInstructionDiscovery", false));
223+
var rootFalse = JsonDocument.Parse(JsonSerializer.Serialize(requestFalse, requestType, options)).RootElement;
224+
Assert.False(rootFalse.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean());
225+
226+
var requestOmitted = CreateInternalRequest(
227+
requestType,
228+
("SessionId", "session-id"));
229+
var rootOmitted = JsonDocument.Parse(JsonSerializer.Serialize(requestOmitted, requestType, options)).RootElement;
230+
Assert.False(rootOmitted.TryGetProperty("enableOnDemandInstructionDiscovery", out _));
231+
}
232+
233+
[Fact]
234+
public void ResumeSessionRequest_CanSerializeEnableOnDemandInstructionDiscovery_WithSdkOptions()
235+
{
236+
var options = GetSerializerOptions();
237+
var requestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest");
238+
239+
var requestTrue = CreateInternalRequest(
240+
requestType,
241+
("SessionId", "session-id"),
242+
("EnableOnDemandInstructionDiscovery", true));
243+
var rootTrue = JsonDocument.Parse(JsonSerializer.Serialize(requestTrue, requestType, options)).RootElement;
244+
Assert.True(rootTrue.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean());
245+
246+
var requestFalse = CreateInternalRequest(
247+
requestType,
248+
("SessionId", "session-id"),
249+
("EnableOnDemandInstructionDiscovery", false));
250+
var rootFalse = JsonDocument.Parse(JsonSerializer.Serialize(requestFalse, requestType, options)).RootElement;
251+
Assert.False(rootFalse.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean());
252+
253+
var requestOmitted = CreateInternalRequest(
254+
requestType,
255+
("SessionId", "session-id"));
256+
var rootOmitted = JsonDocument.Parse(JsonSerializer.Serialize(requestOmitted, requestType, options)).RootElement;
257+
Assert.False(rootOmitted.TryGetProperty("enableOnDemandInstructionDiscovery", out _));
258+
}
259+
206260
[Fact]
207261
public void ResumeSessionRequest_CanSerializeModeRequestFlags_WithSdkOptions()
208262
{

go/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
626626
if config.EnableConfigDiscovery {
627627
req.EnableConfigDiscovery = Bool(true)
628628
}
629+
req.EnableOnDemandInstructionDiscovery = config.EnableOnDemandInstructionDiscovery
629630
req.Tools = config.Tools
630631
wireSystemMessage, transformCallbacks := extractTransformCallbacks(config.SystemMessage)
631632
req.SystemMessage = wireSystemMessage
@@ -834,6 +835,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
834835
if config.EnableConfigDiscovery {
835836
req.EnableConfigDiscovery = Bool(true)
836837
}
838+
req.EnableOnDemandInstructionDiscovery = config.EnableOnDemandInstructionDiscovery
837839
if config.DisableResume {
838840
req.DisableResume = Bool(true)
839841
}

go/client_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,6 +1321,100 @@ func TestResumeSessionRequest_IncludeSubAgentStreamingEvents(t *testing.T) {
13211321
})
13221322
}
13231323

1324+
func TestCreateSessionRequest_EnableOnDemandInstructionDiscovery(t *testing.T) {
1325+
t.Run("forwards explicit true", func(t *testing.T) {
1326+
req := createSessionRequest{
1327+
EnableOnDemandInstructionDiscovery: Bool(true),
1328+
}
1329+
data, err := json.Marshal(req)
1330+
if err != nil {
1331+
t.Fatalf("Failed to marshal: %v", err)
1332+
}
1333+
var m map[string]any
1334+
if err := json.Unmarshal(data, &m); err != nil {
1335+
t.Fatalf("Failed to unmarshal: %v", err)
1336+
}
1337+
if m["enableOnDemandInstructionDiscovery"] != true {
1338+
t.Errorf("Expected enableOnDemandInstructionDiscovery to be true, got %v", m["enableOnDemandInstructionDiscovery"])
1339+
}
1340+
})
1341+
1342+
t.Run("preserves explicit false", func(t *testing.T) {
1343+
req := createSessionRequest{
1344+
EnableOnDemandInstructionDiscovery: Bool(false),
1345+
}
1346+
data, err := json.Marshal(req)
1347+
if err != nil {
1348+
t.Fatalf("Failed to marshal: %v", err)
1349+
}
1350+
var m map[string]any
1351+
if err := json.Unmarshal(data, &m); err != nil {
1352+
t.Fatalf("Failed to unmarshal: %v", err)
1353+
}
1354+
if m["enableOnDemandInstructionDiscovery"] != false {
1355+
t.Errorf("Expected enableOnDemandInstructionDiscovery to be false, got %v", m["enableOnDemandInstructionDiscovery"])
1356+
}
1357+
})
1358+
1359+
t.Run("omits enableOnDemandInstructionDiscovery when not set", func(t *testing.T) {
1360+
req := createSessionRequest{}
1361+
data, _ := json.Marshal(req)
1362+
var m map[string]any
1363+
json.Unmarshal(data, &m)
1364+
if _, ok := m["enableOnDemandInstructionDiscovery"]; ok {
1365+
t.Error("Expected enableOnDemandInstructionDiscovery to be omitted when not set")
1366+
}
1367+
})
1368+
}
1369+
1370+
func TestResumeSessionRequest_EnableOnDemandInstructionDiscovery(t *testing.T) {
1371+
t.Run("forwards explicit true", func(t *testing.T) {
1372+
req := resumeSessionRequest{
1373+
SessionID: "s1",
1374+
EnableOnDemandInstructionDiscovery: Bool(true),
1375+
}
1376+
data, err := json.Marshal(req)
1377+
if err != nil {
1378+
t.Fatalf("Failed to marshal: %v", err)
1379+
}
1380+
var m map[string]any
1381+
if err := json.Unmarshal(data, &m); err != nil {
1382+
t.Fatalf("Failed to unmarshal: %v", err)
1383+
}
1384+
if m["enableOnDemandInstructionDiscovery"] != true {
1385+
t.Errorf("Expected enableOnDemandInstructionDiscovery to be true, got %v", m["enableOnDemandInstructionDiscovery"])
1386+
}
1387+
})
1388+
1389+
t.Run("preserves explicit false", func(t *testing.T) {
1390+
req := resumeSessionRequest{
1391+
SessionID: "s1",
1392+
EnableOnDemandInstructionDiscovery: Bool(false),
1393+
}
1394+
data, err := json.Marshal(req)
1395+
if err != nil {
1396+
t.Fatalf("Failed to marshal: %v", err)
1397+
}
1398+
var m map[string]any
1399+
if err := json.Unmarshal(data, &m); err != nil {
1400+
t.Fatalf("Failed to unmarshal: %v", err)
1401+
}
1402+
if m["enableOnDemandInstructionDiscovery"] != false {
1403+
t.Errorf("Expected enableOnDemandInstructionDiscovery to be false, got %v", m["enableOnDemandInstructionDiscovery"])
1404+
}
1405+
})
1406+
1407+
t.Run("omits enableOnDemandInstructionDiscovery when not set", func(t *testing.T) {
1408+
req := resumeSessionRequest{SessionID: "s1"}
1409+
data, _ := json.Marshal(req)
1410+
var m map[string]any
1411+
json.Unmarshal(data, &m)
1412+
if _, ok := m["enableOnDemandInstructionDiscovery"]; ok {
1413+
t.Error("Expected enableOnDemandInstructionDiscovery to be omitted when not set")
1414+
}
1415+
})
1416+
}
1417+
13241418
func TestCreateSessionResponse_Capabilities(t *testing.T) {
13251419
t.Run("reads capabilities from session.create response", func(t *testing.T) {
13261420
responseJSON := `{"sessionId":"s1","workspacePath":"/tmp","capabilities":{"ui":{"elicitation":true}}}`

go/internal/e2e/client_options_e2e_test.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,10 @@ func TestClientOptionsE2E(t *testing.T) {
198198
}
199199

200200
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
201-
EnableConfigDiscovery: true,
202-
IncludeSubAgentStreamingEvents: copilot.Bool(false),
203-
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
201+
EnableConfigDiscovery: true,
202+
EnableOnDemandInstructionDiscovery: copilot.Bool(true),
203+
IncludeSubAgentStreamingEvents: copilot.Bool(false),
204+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
204205
})
205206
if err != nil {
206207
t.Fatalf("CreateSession failed: %v", err)
@@ -225,6 +226,9 @@ func TestClientOptionsE2E(t *testing.T) {
225226
if v, ok := params["enableConfigDiscovery"].(bool); !ok || v != true {
226227
t.Errorf("Expected session.create.params.enableConfigDiscovery=true, got %v", params["enableConfigDiscovery"])
227228
}
229+
if v, ok := params["enableOnDemandInstructionDiscovery"].(bool); !ok || v != true {
230+
t.Errorf("Expected session.create.params.enableOnDemandInstructionDiscovery=true, got %v", params["enableOnDemandInstructionDiscovery"])
231+
}
228232
if v, ok := params["includeSubAgentStreamingEvents"].(bool); !ok || v != false {
229233
t.Errorf("Expected session.create.params.includeSubAgentStreamingEvents=false, got %v", params["includeSubAgentStreamingEvents"])
230234
}

0 commit comments

Comments
 (0)