Skip to content
Draft
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 @@ -1225,6 +1225,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
RemoteSession: config.RemoteSession,
Cloud: config.Cloud,
InstructionDirectories: config.InstructionDirectories,
CustomAgentDirectories: config.CustomAgentDirectories,
PluginDirectories: config.PluginDirectories,
DisabledMcpServers: config.DisabledMcpServers,
LargeOutput: config.LargeOutput,
Expand Down Expand Up @@ -1446,6 +1447,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
RemoteSession: config.RemoteSession,
ContinuePendingWork: config.ContinuePendingWork,
InstructionDirectories: config.InstructionDirectories,
CustomAgentDirectories: config.CustomAgentDirectories,
PluginDirectories: config.PluginDirectories,
DisabledMcpServers: config.DisabledMcpServers,
LargeOutput: config.LargeOutput,
Expand Down Expand Up @@ -2805,6 +2807,7 @@ internal record CreateSessionRequest(
RemoteSessionMode? RemoteSession = null,
CloudSessionOptions? Cloud = null,
IList<string>? InstructionDirectories = null,
IList<string>? CustomAgentDirectories = null,
IList<string>? PluginDirectories = null,
[property: JsonPropertyName("disabledMcpServers")] IList<string>? DisabledMcpServers = null,
LargeToolOutputConfig? LargeOutput = null,
Expand Down Expand Up @@ -2920,6 +2923,7 @@ internal record ResumeSessionRequest(
RemoteSessionMode? RemoteSession = null,
bool? ContinuePendingWork = null,
IList<string>? InstructionDirectories = null,
IList<string>? CustomAgentDirectories = null,
IList<string>? PluginDirectories = null,
[property: JsonPropertyName("disabledMcpServers")] IList<string>? DisabledMcpServers = null,
LargeToolOutputConfig? LargeOutput = null,
Expand Down
4 changes: 4 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3222,6 +3222,7 @@ protected SessionConfigBase(SessionConfigBase? other)
SkillDirectories = other.SkillDirectories is not null ? [.. other.SkillDirectories] : null;
PluginDirectories = other.PluginDirectories is not null ? [.. other.PluginDirectories] : null;
InstructionDirectories = other.InstructionDirectories is not null ? [.. other.InstructionDirectories] : null;
CustomAgentDirectories = other.CustomAgentDirectories is not null ? [.. other.CustomAgentDirectories] : null;
SessionLimits = other.SessionLimits;
Streaming = other.Streaming;
IncludeSubAgentStreamingEvents = other.IncludeSubAgentStreamingEvents;
Expand Down Expand Up @@ -3583,6 +3584,9 @@ protected SessionConfigBase(SessionConfigBase? other)
/// <summary>Additional directories to search for custom instruction files.</summary>
public IList<string>? InstructionDirectories { get; set; }

/// <summary>Additional directories to search for custom agent files.</summary>
public IList<string>? CustomAgentDirectories { get; set; }

/// <summary>List of skill names to disable.</summary>
public IList<string>? DisabledSkills { get; set; }

Expand Down
58 changes: 58 additions & 0 deletions dotnet/test/E2E/SessionConfigE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,64 @@ await File.WriteAllTextAsync(
await session2.DisposeAsync();
}

[Fact]
public async Task Should_Apply_CustomAgentDirectories_On_Create()
{
var projectDir = Path.Join(Ctx.WorkDir, "agent-create-project");
var agentDir = Path.Join(Ctx.WorkDir, "extra-create-agents");
Directory.CreateDirectory(projectDir);
Directory.CreateDirectory(agentDir);
await File.WriteAllTextAsync(
Path.Join(agentDir, "reviewer.agent.md"),
"---\nname: reviewer\ndescription: Reviews code carefully.\n---\nYou review code carefully.");

var session = await CreateSessionAsync(new SessionConfig
{
WorkingDirectory = projectDir,
CustomAgentDirectories = [agentDir],
});

await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" });

var exchanges = await Ctx.GetExchangesAsync();
Assert.NotEmpty(exchanges);
Assert.Contains("reviewer", GetTaskAgentTypes(exchanges[^1]));

await session.DisposeAsync();
}

[Fact]
public async Task Should_Apply_CustomAgentDirectories_On_Resume()
{
var projectDir = Path.Join(Ctx.WorkDir, "agent-resume-project");
var agentDir = Path.Join(Ctx.WorkDir, "extra-resume-agents");
Directory.CreateDirectory(projectDir);
Directory.CreateDirectory(agentDir);
await File.WriteAllTextAsync(
Path.Join(agentDir, "reviewer.agent.md"),
"---\nname: reviewer\ndescription: Reviews code carefully.\n---\nYou review code carefully.");

await using var session1 = await CreateSessionAsync(new SessionConfig
{
WorkingDirectory = projectDir,
});
var sessionId = session1.SessionId;
await SuspendAndUntrackSessionForResumeAsync(session1);
var session2 = await ResumeSessionAsync(sessionId, new ResumeSessionConfig
{
WorkingDirectory = projectDir,
CustomAgentDirectories = [agentDir],
});

await session2.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" });

var exchanges = await Ctx.GetExchangesAsync();
Assert.NotEmpty(exchanges);
Assert.Contains("reviewer", GetTaskAgentTypes(exchanges[^1]));

await session2.DisposeAsync();
}

[Fact]
public async Task Should_Apply_AvailableTools_On_Session_Resume()
{
Expand Down
9 changes: 9 additions & 0 deletions dotnet/test/Unit/CloneTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
DefaultAgent = new DefaultAgentConfig { ExcludedTools = ["hidden-tool"] },
SkillDirectories = ["/skills"],
InstructionDirectories = ["/instructions"],
CustomAgentDirectories = ["/agents"],
DisabledSkills = ["skill1"],
DisabledMcpServers = ["server1"],
PluginDirectories = ["/plugins"],
Expand Down Expand Up @@ -145,6 +146,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
Assert.Equal(original.DefaultAgent!.ExcludedTools, clone.DefaultAgent!.ExcludedTools);
Assert.Equal(original.SkillDirectories, clone.SkillDirectories);
Assert.Equal(original.InstructionDirectories, clone.InstructionDirectories);
Assert.Equal(original.CustomAgentDirectories, clone.CustomAgentDirectories);
Assert.Equal(original.DisabledSkills, clone.DisabledSkills);
Assert.Equal(original.DisabledMcpServers, clone.DisabledMcpServers);
Assert.Equal(original.PluginDirectories, clone.PluginDirectories);
Expand All @@ -168,6 +170,7 @@ public void SessionConfig_Clone_CollectionsAreIndependent()
AdditionalDirectories = ["/shared"],
SkillDirectories = ["/skills"],
InstructionDirectories = ["/instructions"],
CustomAgentDirectories = ["/agents"],
DisabledSkills = ["skill1"],
DisabledMcpServers = ["server1"],
};
Expand All @@ -183,6 +186,7 @@ public void SessionConfig_Clone_CollectionsAreIndependent()
clone.AdditionalDirectories!.Add("/generated");
clone.SkillDirectories!.Add("/more");
clone.InstructionDirectories!.Add("/more-instructions");
clone.CustomAgentDirectories!.Add("/more-agents");
clone.DisabledSkills!.Add("skill99");
clone.DisabledMcpServers!.Add("server99");

Expand All @@ -195,6 +199,7 @@ public void SessionConfig_Clone_CollectionsAreIndependent()
Assert.Single(original.AdditionalDirectories!);
Assert.Single(original.SkillDirectories!);
Assert.Single(original.InstructionDirectories!);
Assert.Single(original.CustomAgentDirectories!);
Assert.Single(original.DisabledSkills!);
Assert.Single(original.DisabledMcpServers!);
}
Expand Down Expand Up @@ -223,6 +228,7 @@ public void ResumeSessionConfig_Clone_CollectionsAreIndependent()
AdditionalDirectories = ["/shared"],
SkillDirectories = ["/skills"],
InstructionDirectories = ["/instructions"],
CustomAgentDirectories = ["/agents"],
DisabledSkills = ["skill1"],
DisabledMcpServers = ["server1"],
};
Expand All @@ -238,6 +244,7 @@ public void ResumeSessionConfig_Clone_CollectionsAreIndependent()
clone.AdditionalDirectories!.Add("/generated");
clone.SkillDirectories!.Add("/more");
clone.InstructionDirectories!.Add("/more-instructions");
clone.CustomAgentDirectories!.Add("/more-agents");
clone.DisabledSkills!.Add("skill99");
clone.DisabledMcpServers!.Add("server99");

Expand All @@ -250,6 +257,7 @@ public void ResumeSessionConfig_Clone_CollectionsAreIndependent()
Assert.Single(original.AdditionalDirectories!);
Assert.Single(original.SkillDirectories!);
Assert.Single(original.InstructionDirectories!);
Assert.Single(original.CustomAgentDirectories!);
Assert.Single(original.DisabledSkills!);
Assert.Single(original.DisabledMcpServers!);
}
Expand Down Expand Up @@ -311,6 +319,7 @@ public void Clone_WithNullCollections_ReturnsNullCollections()
Assert.Null(clone.CustomAgents);
Assert.Null(clone.SkillDirectories);
Assert.Null(clone.InstructionDirectories);
Assert.Null(clone.CustomAgentDirectories);
Assert.Null(clone.DisabledSkills);
Assert.Null(clone.DisabledMcpServers);
Assert.Null(clone.Tools);
Expand Down
33 changes: 33 additions & 0 deletions dotnet/test/Unit/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,39 @@ public void ResumeSessionRequest_CanSerializeInstructionDirectories_WithSdkOptio
Assert.Equal("C:\\resume-instructions", root.GetProperty("instructionDirectories")[0].GetString());
}

[Fact]
public void CreateSessionRequest_CanSerializeCustomAgentDirectories_WithSdkOptions()
{
var options = GetSerializerOptions();
var requestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest");
var request = CreateInternalRequest(
requestType,
("SessionId", "session-id"),
("CustomAgentDirectories", new List<string> { "C:\\extra-agents", "C:\\more-agents" }));

var json = JsonSerializer.Serialize(request, requestType, options);
using var document = JsonDocument.Parse(json);
var root = document.RootElement;
Assert.Equal("C:\\extra-agents", root.GetProperty("customAgentDirectories")[0].GetString());
Assert.Equal("C:\\more-agents", root.GetProperty("customAgentDirectories")[1].GetString());
}

[Fact]
public void ResumeSessionRequest_CanSerializeCustomAgentDirectories_WithSdkOptions()
{
var options = GetSerializerOptions();
var requestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest");
var request = CreateInternalRequest(
requestType,
("SessionId", "session-id"),
("CustomAgentDirectories", new List<string> { "C:\\resume-agents" }));

var json = JsonSerializer.Serialize(request, requestType, options);
using var document = JsonDocument.Parse(json);
var root = document.RootElement;
Assert.Equal("C:\\resume-agents", root.GetProperty("customAgentDirectories")[0].GetString());
}

[Fact]
public void SessionRequests_CanSerializeCapiOptions_WithSdkOptions()
{
Expand Down
2 changes: 2 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
req.SkillDirectories = config.SkillDirectories
req.PluginDirectories = config.PluginDirectories
req.InstructionDirectories = config.InstructionDirectories
req.CustomAgentDirectories = config.CustomAgentDirectories
req.DisabledSkills = config.DisabledSkills
if config.DisabledMCPServers != nil {
req.DisabledMCPServers = &config.DisabledMCPServers
Expand Down Expand Up @@ -1224,6 +1225,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
req.SkillDirectories = config.SkillDirectories
req.PluginDirectories = config.PluginDirectories
req.InstructionDirectories = config.InstructionDirectories
req.CustomAgentDirectories = config.CustomAgentDirectories
req.DisabledSkills = config.DisabledSkills
if config.DisabledMCPServers != nil {
req.DisabledMCPServers = &config.DisabledMCPServers
Expand Down
59 changes: 59 additions & 0 deletions go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1584,6 +1584,65 @@ func TestResumeSessionRequest_InstructionDirectories(t *testing.T) {
})
}

func TestCreateSessionRequest_CustomAgentDirectories(t *testing.T) {
t.Run("includes customAgentDirectories in JSON when set", func(t *testing.T) {
req := createSessionRequest{CustomAgentDirectories: []string{`C:\extra-agents`, `C:\more-agents`}}
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)
}
got := m["customAgentDirectories"].([]any)
if len(got) != 2 || got[0] != `C:\extra-agents` || got[1] != `C:\more-agents` {
t.Errorf("Expected customAgentDirectories to be serialized, got %v", got)
}
})

t.Run("omits customAgentDirectories from JSON when empty", func(t *testing.T) {
req := createSessionRequest{}
data, _ := json.Marshal(req)
var m map[string]any
json.Unmarshal(data, &m)
if _, ok := m["customAgentDirectories"]; ok {
t.Error("Expected customAgentDirectories to be omitted when empty")
}
})
}

func TestResumeSessionRequest_CustomAgentDirectories(t *testing.T) {
t.Run("includes customAgentDirectories in JSON when set", func(t *testing.T) {
req := resumeSessionRequest{
SessionID: "s1",
CustomAgentDirectories: []string{`C:\resume-agents`},
}
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)
}
got := m["customAgentDirectories"].([]any)
if len(got) != 1 || got[0] != `C:\resume-agents` {
t.Errorf("Expected customAgentDirectories to be serialized, got %v", got)
}
})

t.Run("omits customAgentDirectories from JSON when empty", func(t *testing.T) {
req := resumeSessionRequest{SessionID: "s1"}
data, _ := json.Marshal(req)
var m map[string]any
json.Unmarshal(data, &m)
if _, ok := m["customAgentDirectories"]; ok {
t.Error("Expected customAgentDirectories to be omitted when empty")
}
})
}

func TestCreateSessionRequest_MCPOAuthTokenStorage(t *testing.T) {
t.Run("includes mcpOAuthTokenStorage in JSON when set", func(t *testing.T) {
req := createSessionRequest{MCPOAuthTokenStorage: "in-memory"}
Expand Down
Loading
Loading