Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
config.Agent,
config.ConfigDir,
config.EnableConfigDiscovery,
config.EnableOnDemandInstructionDiscovery,
config.SkillDirectories,
config.DisabledSkills,
config.InfiniteSessions,
Expand Down Expand Up @@ -780,6 +781,7 @@ public async Task<CopilotSession> 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,
Expand Down Expand Up @@ -2020,6 +2022,7 @@ internal record CreateSessionRequest(
string? Agent,
string? ConfigDir,
bool? EnableConfigDiscovery,
bool? EnableOnDemandInstructionDiscovery,
IList<string>? SkillDirectories,
IList<string>? DisabledSkills,
InfiniteSessionConfig? InfiniteSessions,
Expand Down Expand Up @@ -2074,6 +2077,7 @@ internal record ResumeSessionRequest(
string? WorkingDirectory,
string? ConfigDir,
bool? EnableConfigDiscovery,
bool? EnableOnDemandInstructionDiscovery,
bool? DisableResume,
bool? Streaming,
bool? IncludeSubAgentStreamingEvents,
Expand Down
32 changes: 32 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2058,6 +2058,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;
Expand Down Expand Up @@ -2138,6 +2139,25 @@ protected SessionConfig(SessionConfig? other)
/// </summary>
public bool? EnableConfigDiscovery { get; set; }

/// <summary>
/// When <see langword="true"/>, requests on-demand discovery of custom instruction
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does "on-demand discovery" actually mean? Demanded by what / when, discovered where, etc.

/// files after the agent successfully reads or views files. Discovered instruction
/// files are treated as model instructions and may influence agent behavior.
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// </summary>
public bool? EnableOnDemandInstructionDiscovery { get; set; }

/// <summary>
/// Custom tool declarations available to the language model during the session.
/// Declarations backed by an <see cref="AIFunction"/> are invoked automatically; declarations without one
Expand Down Expand Up @@ -2368,6 +2388,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;
Expand Down Expand Up @@ -2528,6 +2549,17 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
/// </summary>
public bool? EnableConfigDiscovery { get; set; }

/// <summary>
/// When <see langword="true"/>, requests on-demand discovery of custom instruction
/// files after the agent successfully reads or views files. See
/// <see cref="SessionConfig.EnableOnDemandInstructionDiscovery"/> for details.
/// <para>
/// For resumed sessions, omitting this option leaves the existing session setting
/// unchanged; set <see langword="false"/> to disable future on-demand discovery.
/// </para>
/// </summary>
public bool? EnableOnDemandInstructionDiscovery { get; set; }

/// <summary>
/// When true, the session.resume event is not emitted.
/// Default: false (resume event is emitted).
Expand Down
48 changes: 48 additions & 0 deletions dotnet/test/Unit/CloneTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
WorkingDirectory = "/workspace",
Streaming = true,
EnableSessionTelemetry = false,
EnableOnDemandInstructionDiscovery = true,
IncludeSubAgentStreamingEvents = false,
McpServers = new Dictionary<string, McpServerConfig> { ["server1"] = new McpStdioServerConfig { Command = "echo" } },
CustomAgents = [new CustomAgentConfig { Name = "agent1", Model = "claude-haiku-4.5" }],
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
54 changes: 54 additions & 0 deletions dotnet/test/Unit/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
2 changes: 2 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,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
Expand Down Expand Up @@ -831,6 +832,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)
}
Expand Down
94 changes: 94 additions & 0 deletions go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1318,6 +1318,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}}}`
Expand Down
10 changes: 7 additions & 3 deletions go/internal/e2e/client_options_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"])
}
Expand Down
Loading
Loading