Skip to content

Commit 1eddcfb

Browse files
timrogersCopilot
andcommitted
Add customAgentDirectories session config option
Add a `customAgentDirectories` option to the session config across all six SDK languages, mirroring the existing `instructionDirectories` / `skillDirectories` passthrough options. It accepts a list of directory paths that the CLI searches for custom agent definition files, forwarded on both session create and resume. - nodejs: SessionConfigBase.customAgentDirectories + create/resume forwarding - go: SessionConfig/ResumeSessionConfig + request structs + forwarding - python: create_session/resume_session custom_agent_directories param - dotnet: SessionConfig.CustomAgentDirectories + Clone + create/resume/update - java: SessionConfig/ResumeSessionConfig + request builders - rust: SessionConfig::with_custom_agent_directories + create/resume wire Unit tests verify the option is forwarded on the session.create and session.resume JSON-RPC requests in every language. End-to-end tests (with handcrafted replay-proxy snapshots) additionally assert that a custom agent placed in a `customAgentDirectories` path is discovered by the CLI and surfaced in the task tool's `agent_type` enum, on both create and resume. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d4c94c34-dda4-49a1-b876-687fc771d0e0
1 parent 0c59943 commit 1eddcfb

33 files changed

Lines changed: 817 additions & 0 deletions

dotnet/src/Client.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,6 +1225,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
12251225
RemoteSession: config.RemoteSession,
12261226
Cloud: config.Cloud,
12271227
InstructionDirectories: config.InstructionDirectories,
1228+
CustomAgentDirectories: config.CustomAgentDirectories,
12281229
PluginDirectories: config.PluginDirectories,
12291230
DisabledMcpServers: config.DisabledMcpServers,
12301231
LargeOutput: config.LargeOutput,
@@ -1446,6 +1447,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
14461447
RemoteSession: config.RemoteSession,
14471448
ContinuePendingWork: config.ContinuePendingWork,
14481449
InstructionDirectories: config.InstructionDirectories,
1450+
CustomAgentDirectories: config.CustomAgentDirectories,
14491451
PluginDirectories: config.PluginDirectories,
14501452
DisabledMcpServers: config.DisabledMcpServers,
14511453
LargeOutput: config.LargeOutput,
@@ -2805,6 +2807,7 @@ internal record CreateSessionRequest(
28052807
RemoteSessionMode? RemoteSession = null,
28062808
CloudSessionOptions? Cloud = null,
28072809
IList<string>? InstructionDirectories = null,
2810+
IList<string>? CustomAgentDirectories = null,
28082811
IList<string>? PluginDirectories = null,
28092812
[property: JsonPropertyName("disabledMcpServers")] IList<string>? DisabledMcpServers = null,
28102813
LargeToolOutputConfig? LargeOutput = null,
@@ -2920,6 +2923,7 @@ internal record ResumeSessionRequest(
29202923
RemoteSessionMode? RemoteSession = null,
29212924
bool? ContinuePendingWork = null,
29222925
IList<string>? InstructionDirectories = null,
2926+
IList<string>? CustomAgentDirectories = null,
29232927
IList<string>? PluginDirectories = null,
29242928
[property: JsonPropertyName("disabledMcpServers")] IList<string>? DisabledMcpServers = null,
29252929
LargeToolOutputConfig? LargeOutput = null,

dotnet/src/Types.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3222,6 +3222,7 @@ protected SessionConfigBase(SessionConfigBase? other)
32223222
SkillDirectories = other.SkillDirectories is not null ? [.. other.SkillDirectories] : null;
32233223
PluginDirectories = other.PluginDirectories is not null ? [.. other.PluginDirectories] : null;
32243224
InstructionDirectories = other.InstructionDirectories is not null ? [.. other.InstructionDirectories] : null;
3225+
CustomAgentDirectories = other.CustomAgentDirectories is not null ? [.. other.CustomAgentDirectories] : null;
32253226
SessionLimits = other.SessionLimits;
32263227
Streaming = other.Streaming;
32273228
IncludeSubAgentStreamingEvents = other.IncludeSubAgentStreamingEvents;
@@ -3583,6 +3584,9 @@ protected SessionConfigBase(SessionConfigBase? other)
35833584
/// <summary>Additional directories to search for custom instruction files.</summary>
35843585
public IList<string>? InstructionDirectories { get; set; }
35853586

3587+
/// <summary>Additional directories to search for custom agent files.</summary>
3588+
public IList<string>? CustomAgentDirectories { get; set; }
3589+
35863590
/// <summary>List of skill names to disable.</summary>
35873591
public IList<string>? DisabledSkills { get; set; }
35883592

dotnet/test/E2E/SessionConfigE2ETests.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,64 @@ await File.WriteAllTextAsync(
446446
await session2.DisposeAsync();
447447
}
448448

449+
[Fact]
450+
public async Task Should_Apply_CustomAgentDirectories_On_Create()
451+
{
452+
var projectDir = Path.Join(Ctx.WorkDir, "agent-create-project");
453+
var agentDir = Path.Join(Ctx.WorkDir, "extra-create-agents");
454+
Directory.CreateDirectory(projectDir);
455+
Directory.CreateDirectory(agentDir);
456+
await File.WriteAllTextAsync(
457+
Path.Join(agentDir, "reviewer.agent.md"),
458+
"---\nname: reviewer\ndescription: Reviews code carefully.\n---\nYou review code carefully.");
459+
460+
var session = await CreateSessionAsync(new SessionConfig
461+
{
462+
WorkingDirectory = projectDir,
463+
CustomAgentDirectories = [agentDir],
464+
});
465+
466+
await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" });
467+
468+
var exchanges = await Ctx.GetExchangesAsync();
469+
Assert.NotEmpty(exchanges);
470+
Assert.Contains("reviewer", GetTaskAgentTypes(exchanges[^1]));
471+
472+
await session.DisposeAsync();
473+
}
474+
475+
[Fact]
476+
public async Task Should_Apply_CustomAgentDirectories_On_Resume()
477+
{
478+
var projectDir = Path.Join(Ctx.WorkDir, "agent-resume-project");
479+
var agentDir = Path.Join(Ctx.WorkDir, "extra-resume-agents");
480+
Directory.CreateDirectory(projectDir);
481+
Directory.CreateDirectory(agentDir);
482+
await File.WriteAllTextAsync(
483+
Path.Join(agentDir, "reviewer.agent.md"),
484+
"---\nname: reviewer\ndescription: Reviews code carefully.\n---\nYou review code carefully.");
485+
486+
await using var session1 = await CreateSessionAsync(new SessionConfig
487+
{
488+
WorkingDirectory = projectDir,
489+
});
490+
var sessionId = session1.SessionId;
491+
await SuspendAndUntrackSessionForResumeAsync(session1);
492+
var session2 = await ResumeSessionAsync(sessionId, new ResumeSessionConfig
493+
{
494+
WorkingDirectory = projectDir,
495+
CustomAgentDirectories = [agentDir],
496+
});
497+
498+
await session2.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" });
499+
500+
var exchanges = await Ctx.GetExchangesAsync();
501+
Assert.NotEmpty(exchanges);
502+
Assert.Contains("reviewer", GetTaskAgentTypes(exchanges[^1]));
503+
504+
await session2.DisposeAsync();
505+
}
506+
449507
[Fact]
450508
public async Task Should_Apply_AvailableTools_On_Session_Resume()
451509
{

dotnet/test/Unit/CloneTests.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
103103
DefaultAgent = new DefaultAgentConfig { ExcludedTools = ["hidden-tool"] },
104104
SkillDirectories = ["/skills"],
105105
InstructionDirectories = ["/instructions"],
106+
CustomAgentDirectories = ["/agents"],
106107
DisabledSkills = ["skill1"],
107108
DisabledMcpServers = ["server1"],
108109
PluginDirectories = ["/plugins"],
@@ -145,6 +146,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
145146
Assert.Equal(original.DefaultAgent!.ExcludedTools, clone.DefaultAgent!.ExcludedTools);
146147
Assert.Equal(original.SkillDirectories, clone.SkillDirectories);
147148
Assert.Equal(original.InstructionDirectories, clone.InstructionDirectories);
149+
Assert.Equal(original.CustomAgentDirectories, clone.CustomAgentDirectories);
148150
Assert.Equal(original.DisabledSkills, clone.DisabledSkills);
149151
Assert.Equal(original.DisabledMcpServers, clone.DisabledMcpServers);
150152
Assert.Equal(original.PluginDirectories, clone.PluginDirectories);
@@ -168,6 +170,7 @@ public void SessionConfig_Clone_CollectionsAreIndependent()
168170
AdditionalDirectories = ["/shared"],
169171
SkillDirectories = ["/skills"],
170172
InstructionDirectories = ["/instructions"],
173+
CustomAgentDirectories = ["/agents"],
171174
DisabledSkills = ["skill1"],
172175
DisabledMcpServers = ["server1"],
173176
};
@@ -183,6 +186,7 @@ public void SessionConfig_Clone_CollectionsAreIndependent()
183186
clone.AdditionalDirectories!.Add("/generated");
184187
clone.SkillDirectories!.Add("/more");
185188
clone.InstructionDirectories!.Add("/more-instructions");
189+
clone.CustomAgentDirectories!.Add("/more-agents");
186190
clone.DisabledSkills!.Add("skill99");
187191
clone.DisabledMcpServers!.Add("server99");
188192

@@ -195,6 +199,7 @@ public void SessionConfig_Clone_CollectionsAreIndependent()
195199
Assert.Single(original.AdditionalDirectories!);
196200
Assert.Single(original.SkillDirectories!);
197201
Assert.Single(original.InstructionDirectories!);
202+
Assert.Single(original.CustomAgentDirectories!);
198203
Assert.Single(original.DisabledSkills!);
199204
Assert.Single(original.DisabledMcpServers!);
200205
}
@@ -223,6 +228,7 @@ public void ResumeSessionConfig_Clone_CollectionsAreIndependent()
223228
AdditionalDirectories = ["/shared"],
224229
SkillDirectories = ["/skills"],
225230
InstructionDirectories = ["/instructions"],
231+
CustomAgentDirectories = ["/agents"],
226232
DisabledSkills = ["skill1"],
227233
DisabledMcpServers = ["server1"],
228234
};
@@ -238,6 +244,7 @@ public void ResumeSessionConfig_Clone_CollectionsAreIndependent()
238244
clone.AdditionalDirectories!.Add("/generated");
239245
clone.SkillDirectories!.Add("/more");
240246
clone.InstructionDirectories!.Add("/more-instructions");
247+
clone.CustomAgentDirectories!.Add("/more-agents");
241248
clone.DisabledSkills!.Add("skill99");
242249
clone.DisabledMcpServers!.Add("server99");
243250

@@ -250,6 +257,7 @@ public void ResumeSessionConfig_Clone_CollectionsAreIndependent()
250257
Assert.Single(original.AdditionalDirectories!);
251258
Assert.Single(original.SkillDirectories!);
252259
Assert.Single(original.InstructionDirectories!);
260+
Assert.Single(original.CustomAgentDirectories!);
253261
Assert.Single(original.DisabledSkills!);
254262
Assert.Single(original.DisabledMcpServers!);
255263
}
@@ -311,6 +319,7 @@ public void Clone_WithNullCollections_ReturnsNullCollections()
311319
Assert.Null(clone.CustomAgents);
312320
Assert.Null(clone.SkillDirectories);
313321
Assert.Null(clone.InstructionDirectories);
322+
Assert.Null(clone.CustomAgentDirectories);
314323
Assert.Null(clone.DisabledSkills);
315324
Assert.Null(clone.DisabledMcpServers);
316325
Assert.Null(clone.Tools);

dotnet/test/Unit/SerializationTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,39 @@ public void ResumeSessionRequest_CanSerializeInstructionDirectories_WithSdkOptio
248248
Assert.Equal("C:\\resume-instructions", root.GetProperty("instructionDirectories")[0].GetString());
249249
}
250250

251+
[Fact]
252+
public void CreateSessionRequest_CanSerializeCustomAgentDirectories_WithSdkOptions()
253+
{
254+
var options = GetSerializerOptions();
255+
var requestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest");
256+
var request = CreateInternalRequest(
257+
requestType,
258+
("SessionId", "session-id"),
259+
("CustomAgentDirectories", new List<string> { "C:\\extra-agents", "C:\\more-agents" }));
260+
261+
var json = JsonSerializer.Serialize(request, requestType, options);
262+
using var document = JsonDocument.Parse(json);
263+
var root = document.RootElement;
264+
Assert.Equal("C:\\extra-agents", root.GetProperty("customAgentDirectories")[0].GetString());
265+
Assert.Equal("C:\\more-agents", root.GetProperty("customAgentDirectories")[1].GetString());
266+
}
267+
268+
[Fact]
269+
public void ResumeSessionRequest_CanSerializeCustomAgentDirectories_WithSdkOptions()
270+
{
271+
var options = GetSerializerOptions();
272+
var requestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest");
273+
var request = CreateInternalRequest(
274+
requestType,
275+
("SessionId", "session-id"),
276+
("CustomAgentDirectories", new List<string> { "C:\\resume-agents" }));
277+
278+
var json = JsonSerializer.Serialize(request, requestType, options);
279+
using var document = JsonDocument.Parse(json);
280+
var root = document.RootElement;
281+
Assert.Equal("C:\\resume-agents", root.GetProperty("customAgentDirectories")[0].GetString());
282+
}
283+
251284
[Fact]
252285
public void SessionRequests_CanSerializeCapiOptions_WithSdkOptions()
253286
{

go/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
840840
req.SkillDirectories = config.SkillDirectories
841841
req.PluginDirectories = config.PluginDirectories
842842
req.InstructionDirectories = config.InstructionDirectories
843+
req.CustomAgentDirectories = config.CustomAgentDirectories
843844
req.DisabledSkills = config.DisabledSkills
844845
if config.DisabledMCPServers != nil {
845846
req.DisabledMCPServers = &config.DisabledMCPServers
@@ -1224,6 +1225,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
12241225
req.SkillDirectories = config.SkillDirectories
12251226
req.PluginDirectories = config.PluginDirectories
12261227
req.InstructionDirectories = config.InstructionDirectories
1228+
req.CustomAgentDirectories = config.CustomAgentDirectories
12271229
req.DisabledSkills = config.DisabledSkills
12281230
if config.DisabledMCPServers != nil {
12291231
req.DisabledMCPServers = &config.DisabledMCPServers

go/client_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,6 +1584,65 @@ func TestResumeSessionRequest_InstructionDirectories(t *testing.T) {
15841584
})
15851585
}
15861586

1587+
func TestCreateSessionRequest_CustomAgentDirectories(t *testing.T) {
1588+
t.Run("includes customAgentDirectories in JSON when set", func(t *testing.T) {
1589+
req := createSessionRequest{CustomAgentDirectories: []string{`C:\extra-agents`, `C:\more-agents`}}
1590+
data, err := json.Marshal(req)
1591+
if err != nil {
1592+
t.Fatalf("Failed to marshal: %v", err)
1593+
}
1594+
var m map[string]any
1595+
if err := json.Unmarshal(data, &m); err != nil {
1596+
t.Fatalf("Failed to unmarshal: %v", err)
1597+
}
1598+
got := m["customAgentDirectories"].([]any)
1599+
if len(got) != 2 || got[0] != `C:\extra-agents` || got[1] != `C:\more-agents` {
1600+
t.Errorf("Expected customAgentDirectories to be serialized, got %v", got)
1601+
}
1602+
})
1603+
1604+
t.Run("omits customAgentDirectories from JSON when empty", func(t *testing.T) {
1605+
req := createSessionRequest{}
1606+
data, _ := json.Marshal(req)
1607+
var m map[string]any
1608+
json.Unmarshal(data, &m)
1609+
if _, ok := m["customAgentDirectories"]; ok {
1610+
t.Error("Expected customAgentDirectories to be omitted when empty")
1611+
}
1612+
})
1613+
}
1614+
1615+
func TestResumeSessionRequest_CustomAgentDirectories(t *testing.T) {
1616+
t.Run("includes customAgentDirectories in JSON when set", func(t *testing.T) {
1617+
req := resumeSessionRequest{
1618+
SessionID: "s1",
1619+
CustomAgentDirectories: []string{`C:\resume-agents`},
1620+
}
1621+
data, err := json.Marshal(req)
1622+
if err != nil {
1623+
t.Fatalf("Failed to marshal: %v", err)
1624+
}
1625+
var m map[string]any
1626+
if err := json.Unmarshal(data, &m); err != nil {
1627+
t.Fatalf("Failed to unmarshal: %v", err)
1628+
}
1629+
got := m["customAgentDirectories"].([]any)
1630+
if len(got) != 1 || got[0] != `C:\resume-agents` {
1631+
t.Errorf("Expected customAgentDirectories to be serialized, got %v", got)
1632+
}
1633+
})
1634+
1635+
t.Run("omits customAgentDirectories from JSON when empty", func(t *testing.T) {
1636+
req := resumeSessionRequest{SessionID: "s1"}
1637+
data, _ := json.Marshal(req)
1638+
var m map[string]any
1639+
json.Unmarshal(data, &m)
1640+
if _, ok := m["customAgentDirectories"]; ok {
1641+
t.Error("Expected customAgentDirectories to be omitted when empty")
1642+
}
1643+
})
1644+
}
1645+
15871646
func TestCreateSessionRequest_MCPOAuthTokenStorage(t *testing.T) {
15881647
t.Run("includes mcpOAuthTokenStorage in JSON when set", func(t *testing.T) {
15891648
req := createSessionRequest{MCPOAuthTokenStorage: "in-memory"}

0 commit comments

Comments
 (0)