Skip to content

Commit 4633cc2

Browse files
examonTomas MeszarosCopilot
authored
Expose enableOnDemandInstructionDiscovery across all SDK SessionConfig types (#1323)
* 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> * Remove resume-specific docs from create-side SessionConfig Addresses review feedback on PR #1323: the create-session API docs for enable_on_demand_instruction_discovery / EnableOnDemandInstructionDiscovery should not include resume-specific behavior. That note already lives on the resume-side configs (Python resume_session and Go ResumeSessionConfig). * Clarify enableOnDemandInstructionDiscovery docs across SDKs Explain the concrete discovery mechanic (directory walk from the accessed file up to the repo root, applyTo glob filtering, once-per-session delivery) and align the runtime-gating and security wording across all five SDKs. Addresses review feedback on PR #1323. --------- Co-authored-by: Tomas Meszaros <t@m.tm> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4463df4 commit 4633cc2

10 files changed

Lines changed: 326 additions & 3 deletions

File tree

dotnet/test/Unit/CloneTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
7575
WorkingDirectory = "/workspace",
7676
Streaming = true,
7777
EnableSessionTelemetry = false,
78+
EnableOnDemandInstructionDiscovery = true,
7879
IncludeSubAgentStreamingEvents = false,
7980
McpServers = new Dictionary<string, McpServerConfig> { ["server1"] = new McpStdioServerConfig { Command = "echo" } },
8081
McpOAuthTokenStorage = McpOAuthTokenStorageMode.Persistent,
@@ -112,6 +113,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
112113
Assert.Equal(original.WorkingDirectory, clone.WorkingDirectory);
113114
Assert.Equal(original.Streaming, clone.Streaming);
114115
Assert.Equal(original.EnableSessionTelemetry, clone.EnableSessionTelemetry);
116+
Assert.Equal(original.EnableOnDemandInstructionDiscovery, clone.EnableOnDemandInstructionDiscovery);
115117
Assert.Equal(original.IncludeSubAgentStreamingEvents, clone.IncludeSubAgentStreamingEvents);
116118
Assert.Equal(original.McpServers.Count, clone.McpServers!.Count);
117119
Assert.Equal(original.McpOAuthTokenStorage, clone.McpOAuthTokenStorage);
@@ -423,6 +425,52 @@ public void ResumeSessionConfig_Clone_PreservesEnableSessionTelemetryDefault()
423425
Assert.Null(clone.EnableSessionTelemetry);
424426
}
425427

428+
[Fact]
429+
public void SessionConfig_Clone_CopiesEnableOnDemandInstructionDiscovery()
430+
{
431+
var original = new SessionConfig
432+
{
433+
EnableOnDemandInstructionDiscovery = false,
434+
};
435+
436+
var clone = original.Clone();
437+
438+
Assert.False(clone.EnableOnDemandInstructionDiscovery);
439+
}
440+
441+
[Fact]
442+
public void ResumeSessionConfig_Clone_CopiesEnableOnDemandInstructionDiscovery()
443+
{
444+
var original = new ResumeSessionConfig
445+
{
446+
EnableOnDemandInstructionDiscovery = true,
447+
};
448+
449+
var clone = original.Clone();
450+
451+
Assert.True(clone.EnableOnDemandInstructionDiscovery);
452+
}
453+
454+
[Fact]
455+
public void SessionConfig_Clone_PreservesEnableOnDemandInstructionDiscoveryDefault()
456+
{
457+
var original = new SessionConfig();
458+
459+
var clone = original.Clone();
460+
461+
Assert.Null(clone.EnableOnDemandInstructionDiscovery);
462+
}
463+
464+
[Fact]
465+
public void ResumeSessionConfig_Clone_PreservesEnableOnDemandInstructionDiscoveryDefault()
466+
{
467+
var original = new ResumeSessionConfig();
468+
469+
var clone = original.Clone();
470+
471+
Assert.Null(clone.EnableOnDemandInstructionDiscovery);
472+
}
473+
426474
[Fact]
427475
public void SessionConfig_Clone_CopiesMcpOAuthTokenStorage()
428476
{

dotnet/test/Unit/SerializationTests.cs

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

277+
[Fact]
278+
public void CreateSessionRequest_CanSerializeEnableOnDemandInstructionDiscovery_WithSdkOptions()
279+
{
280+
var options = GetSerializerOptions();
281+
var requestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest");
282+
283+
var requestTrue = CreateInternalRequest(
284+
requestType,
285+
("SessionId", "session-id"),
286+
("EnableOnDemandInstructionDiscovery", true));
287+
var rootTrue = JsonDocument.Parse(JsonSerializer.Serialize(requestTrue, requestType, options)).RootElement;
288+
Assert.True(rootTrue.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean());
289+
290+
var requestFalse = CreateInternalRequest(
291+
requestType,
292+
("SessionId", "session-id"),
293+
("EnableOnDemandInstructionDiscovery", false));
294+
var rootFalse = JsonDocument.Parse(JsonSerializer.Serialize(requestFalse, requestType, options)).RootElement;
295+
Assert.False(rootFalse.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean());
296+
297+
var requestOmitted = CreateInternalRequest(
298+
requestType,
299+
("SessionId", "session-id"));
300+
var rootOmitted = JsonDocument.Parse(JsonSerializer.Serialize(requestOmitted, requestType, options)).RootElement;
301+
Assert.False(rootOmitted.TryGetProperty("enableOnDemandInstructionDiscovery", out _));
302+
}
303+
304+
[Fact]
305+
public void ResumeSessionRequest_CanSerializeEnableOnDemandInstructionDiscovery_WithSdkOptions()
306+
{
307+
var options = GetSerializerOptions();
308+
var requestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest");
309+
310+
var requestTrue = CreateInternalRequest(
311+
requestType,
312+
("SessionId", "session-id"),
313+
("EnableOnDemandInstructionDiscovery", true));
314+
var rootTrue = JsonDocument.Parse(JsonSerializer.Serialize(requestTrue, requestType, options)).RootElement;
315+
Assert.True(rootTrue.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean());
316+
317+
var requestFalse = CreateInternalRequest(
318+
requestType,
319+
("SessionId", "session-id"),
320+
("EnableOnDemandInstructionDiscovery", false));
321+
var rootFalse = JsonDocument.Parse(JsonSerializer.Serialize(requestFalse, requestType, options)).RootElement;
322+
Assert.False(rootFalse.GetProperty("enableOnDemandInstructionDiscovery").GetBoolean());
323+
324+
var requestOmitted = CreateInternalRequest(
325+
requestType,
326+
("SessionId", "session-id"));
327+
var rootOmitted = JsonDocument.Parse(JsonSerializer.Serialize(requestOmitted, requestType, options)).RootElement;
328+
Assert.False(rootOmitted.TryGetProperty("enableOnDemandInstructionDiscovery", out _));
329+
}
330+
277331
[Fact]
278332
public void ResumeSessionRequest_CanSerializeOpenCanvases_WithSdkOptions()
279333
{

go/client_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,6 +1445,100 @@ func TestResumeSessionRequest_IncludeSubAgentStreamingEvents(t *testing.T) {
14451445
})
14461446
}
14471447

1448+
func TestCreateSessionRequest_EnableOnDemandInstructionDiscovery(t *testing.T) {
1449+
t.Run("forwards explicit true", func(t *testing.T) {
1450+
req := createSessionRequest{
1451+
EnableOnDemandInstructionDiscovery: Bool(true),
1452+
}
1453+
data, err := json.Marshal(req)
1454+
if err != nil {
1455+
t.Fatalf("Failed to marshal: %v", err)
1456+
}
1457+
var m map[string]any
1458+
if err := json.Unmarshal(data, &m); err != nil {
1459+
t.Fatalf("Failed to unmarshal: %v", err)
1460+
}
1461+
if m["enableOnDemandInstructionDiscovery"] != true {
1462+
t.Errorf("Expected enableOnDemandInstructionDiscovery to be true, got %v", m["enableOnDemandInstructionDiscovery"])
1463+
}
1464+
})
1465+
1466+
t.Run("preserves explicit false", func(t *testing.T) {
1467+
req := createSessionRequest{
1468+
EnableOnDemandInstructionDiscovery: Bool(false),
1469+
}
1470+
data, err := json.Marshal(req)
1471+
if err != nil {
1472+
t.Fatalf("Failed to marshal: %v", err)
1473+
}
1474+
var m map[string]any
1475+
if err := json.Unmarshal(data, &m); err != nil {
1476+
t.Fatalf("Failed to unmarshal: %v", err)
1477+
}
1478+
if m["enableOnDemandInstructionDiscovery"] != false {
1479+
t.Errorf("Expected enableOnDemandInstructionDiscovery to be false, got %v", m["enableOnDemandInstructionDiscovery"])
1480+
}
1481+
})
1482+
1483+
t.Run("omits enableOnDemandInstructionDiscovery when not set", func(t *testing.T) {
1484+
req := createSessionRequest{}
1485+
data, _ := json.Marshal(req)
1486+
var m map[string]any
1487+
json.Unmarshal(data, &m)
1488+
if _, ok := m["enableOnDemandInstructionDiscovery"]; ok {
1489+
t.Error("Expected enableOnDemandInstructionDiscovery to be omitted when not set")
1490+
}
1491+
})
1492+
}
1493+
1494+
func TestResumeSessionRequest_EnableOnDemandInstructionDiscovery(t *testing.T) {
1495+
t.Run("forwards explicit true", func(t *testing.T) {
1496+
req := resumeSessionRequest{
1497+
SessionID: "s1",
1498+
EnableOnDemandInstructionDiscovery: Bool(true),
1499+
}
1500+
data, err := json.Marshal(req)
1501+
if err != nil {
1502+
t.Fatalf("Failed to marshal: %v", err)
1503+
}
1504+
var m map[string]any
1505+
if err := json.Unmarshal(data, &m); err != nil {
1506+
t.Fatalf("Failed to unmarshal: %v", err)
1507+
}
1508+
if m["enableOnDemandInstructionDiscovery"] != true {
1509+
t.Errorf("Expected enableOnDemandInstructionDiscovery to be true, got %v", m["enableOnDemandInstructionDiscovery"])
1510+
}
1511+
})
1512+
1513+
t.Run("preserves explicit false", func(t *testing.T) {
1514+
req := resumeSessionRequest{
1515+
SessionID: "s1",
1516+
EnableOnDemandInstructionDiscovery: Bool(false),
1517+
}
1518+
data, err := json.Marshal(req)
1519+
if err != nil {
1520+
t.Fatalf("Failed to marshal: %v", err)
1521+
}
1522+
var m map[string]any
1523+
if err := json.Unmarshal(data, &m); err != nil {
1524+
t.Fatalf("Failed to unmarshal: %v", err)
1525+
}
1526+
if m["enableOnDemandInstructionDiscovery"] != false {
1527+
t.Errorf("Expected enableOnDemandInstructionDiscovery to be false, got %v", m["enableOnDemandInstructionDiscovery"])
1528+
}
1529+
})
1530+
1531+
t.Run("omits enableOnDemandInstructionDiscovery when not set", func(t *testing.T) {
1532+
req := resumeSessionRequest{SessionID: "s1"}
1533+
data, _ := json.Marshal(req)
1534+
var m map[string]any
1535+
json.Unmarshal(data, &m)
1536+
if _, ok := m["enableOnDemandInstructionDiscovery"]; ok {
1537+
t.Error("Expected enableOnDemandInstructionDiscovery to be omitted when not set")
1538+
}
1539+
})
1540+
}
1541+
14481542
func TestCreateSessionResponse_Capabilities(t *testing.T) {
14491543
t.Run("reads capabilities from session.create response", func(t *testing.T) {
14501544
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
@@ -159,9 +159,10 @@ func TestClientOptionsE2E(t *testing.T) {
159159
}
160160

161161
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
162-
EnableConfigDiscovery: true,
163-
IncludeSubAgentStreamingEvents: copilot.Bool(false),
164-
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
162+
EnableConfigDiscovery: true,
163+
EnableOnDemandInstructionDiscovery: copilot.Bool(true),
164+
IncludeSubAgentStreamingEvents: copilot.Bool(false),
165+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
165166
})
166167
if err != nil {
167168
t.Fatalf("CreateSession failed: %v", err)
@@ -187,6 +188,9 @@ func TestClientOptionsE2E(t *testing.T) {
187188
if v, ok := params["enableConfigDiscovery"].(bool); !ok || v != true {
188189
t.Errorf("Expected session.create.params.enableConfigDiscovery=true, got %v", params["enableConfigDiscovery"])
189190
}
191+
if v, ok := params["enableOnDemandInstructionDiscovery"].(bool); !ok || v != true {
192+
t.Errorf("Expected session.create.params.enableOnDemandInstructionDiscovery=true, got %v", params["enableOnDemandInstructionDiscovery"])
193+
}
190194
if v, ok := params["includeSubAgentStreamingEvents"].(bool); !ok || v != false {
191195
t.Errorf("Expected session.create.params.includeSubAgentStreamingEvents=false, got %v", params["includeSubAgentStreamingEvents"])
192196
}

nodejs/test/client.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,50 @@ describe("CopilotClient", () => {
433433
spy.mockRestore();
434434
});
435435

436+
it("forwards enableOnDemandInstructionDiscovery in session.create request", async () => {
437+
const client = new CopilotClient();
438+
await client.start();
439+
onTestFinished(() => client.forceStop());
440+
441+
const spy = vi.spyOn((client as any).connection!, "sendRequest");
442+
await client.createSession({
443+
enableOnDemandInstructionDiscovery: false,
444+
onPermissionRequest: approveAll,
445+
});
446+
447+
expect(spy).toHaveBeenCalledWith(
448+
"session.create",
449+
expect.objectContaining({ enableOnDemandInstructionDiscovery: false })
450+
);
451+
});
452+
453+
it("forwards enableOnDemandInstructionDiscovery in session.resume request", async () => {
454+
const client = new CopilotClient();
455+
await client.start();
456+
onTestFinished(() => client.forceStop());
457+
458+
const session = await client.createSession({ onPermissionRequest: approveAll });
459+
const spy = vi
460+
.spyOn((client as any).connection!, "sendRequest")
461+
.mockImplementation(async (method: string, params: any) => {
462+
if (method === "session.resume") return { sessionId: params.sessionId };
463+
throw new Error(`Unexpected method: ${method}`);
464+
});
465+
await client.resumeSession(session.sessionId, {
466+
enableOnDemandInstructionDiscovery: false,
467+
onPermissionRequest: approveAll,
468+
});
469+
470+
expect(spy).toHaveBeenCalledWith(
471+
"session.resume",
472+
expect.objectContaining({
473+
enableOnDemandInstructionDiscovery: false,
474+
sessionId: session.sessionId,
475+
})
476+
);
477+
spy.mockRestore();
478+
});
479+
436480
it("defaults includeSubAgentStreamingEvents to true in session.create when not specified", async () => {
437481
const client = new CopilotClient();
438482
await client.start();

nodejs/test/e2e/client_options.e2e.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ describe("Client options", async () => {
291291
const session = await client.createSession({
292292
onPermissionRequest: approveAll,
293293
enableConfigDiscovery: true,
294+
enableOnDemandInstructionDiscovery: true,
294295
includeSubAgentStreamingEvents: false,
295296
});
296297

@@ -300,13 +301,15 @@ describe("Client options", async () => {
300301
method: string;
301302
params: {
302303
enableConfigDiscovery?: boolean;
304+
enableOnDemandInstructionDiscovery?: boolean;
303305
includeSubAgentStreamingEvents?: boolean;
304306
};
305307
}[];
306308
};
307309
const createRequests = updated.requests.filter((r) => r.method === "session.create");
308310
expect(createRequests).toHaveLength(1);
309311
expect(createRequests[0].params.enableConfigDiscovery).toBe(true);
312+
expect(createRequests[0].params.enableOnDemandInstructionDiscovery).toBe(true);
310313
expect(createRequests[0].params.includeSubAgentStreamingEvents).toBe(false);
311314

312315
await session.disconnect();

python/e2e/test_client_options_e2e.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ async def test_should_propagate_process_options_to_spawned_cli(self, ctx: E2ETes
254254
session = await client.create_session(
255255
on_permission_request=PermissionHandler.approve_all,
256256
enable_config_discovery=True,
257+
enable_on_demand_instruction_discovery=True,
257258
include_sub_agent_streaming_events=False,
258259
)
259260
try:
@@ -264,6 +265,7 @@ async def test_should_propagate_process_options_to_spawned_cli(self, ctx: E2ETes
264265
)
265266
params = create_request["params"]
266267
assert params["enableConfigDiscovery"] is True
268+
assert params["enableOnDemandInstructionDiscovery"] is True
267269
assert params["includeSubAgentStreamingEvents"] is False
268270
finally:
269271
await session.disconnect()

0 commit comments

Comments
 (0)