Skip to content

Commit d3abfa2

Browse files
stephentoubCopilot
andauthored
Add instructionDirectories session config support (#1190)
Expose the runtime instructionDirectories session option across the SDKs and cover create/resume forwarding with unit and E2E tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e0dd37b commit d3abfa2

23 files changed

Lines changed: 593 additions & 4 deletions

dotnet/src/Client.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,8 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
506506
Traceparent: traceparent,
507507
Tracestate: tracestate,
508508
ModelCapabilities: config.ModelCapabilities,
509-
GitHubToken: config.GitHubToken);
509+
GitHubToken: config.GitHubToken,
510+
InstructionDirectories: config.InstructionDirectories);
510511

511512
var response = await InvokeRpcAsync<CreateSessionResponse>(
512513
connection.Rpc, "session.create", [request], cancellationToken);
@@ -633,7 +634,8 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
633634
Tracestate: tracestate,
634635
ModelCapabilities: config.ModelCapabilities,
635636
GitHubToken: config.GitHubToken,
636-
ContinuePendingWork: config.ContinuePendingWork);
637+
ContinuePendingWork: config.ContinuePendingWork,
638+
InstructionDirectories: config.InstructionDirectories);
637639

638640
var response = await InvokeRpcAsync<ResumeSessionResponse>(
639641
connection.Rpc, "session.resume", [request], cancellationToken);
@@ -1650,7 +1652,8 @@ internal record CreateSessionRequest(
16501652
string? Traceparent = null,
16511653
string? Tracestate = null,
16521654
ModelCapabilitiesOverride? ModelCapabilities = null,
1653-
string? GitHubToken = null);
1655+
string? GitHubToken = null,
1656+
IList<string>? InstructionDirectories = null);
16541657

16551658
internal record ToolDefinition(
16561659
string Name,
@@ -1707,7 +1710,8 @@ internal record ResumeSessionRequest(
17071710
string? Tracestate = null,
17081711
ModelCapabilitiesOverride? ModelCapabilities = null,
17091712
string? GitHubToken = null,
1710-
bool? ContinuePendingWork = null);
1713+
bool? ContinuePendingWork = null,
1714+
IList<string>? InstructionDirectories = null);
17111715

17121716
internal record ResumeSessionResponse(
17131717
string SessionId,

dotnet/src/Types.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1800,6 +1800,7 @@ protected SessionConfig(SessionConfig? other)
18001800
GitHubToken = other.GitHubToken;
18011801
SessionId = other.SessionId;
18021802
SkillDirectories = other.SkillDirectories is not null ? [.. other.SkillDirectories] : null;
1803+
InstructionDirectories = other.InstructionDirectories is not null ? [.. other.InstructionDirectories] : null;
18031804
Streaming = other.Streaming;
18041805
IncludeSubAgentStreamingEvents = other.IncludeSubAgentStreamingEvents;
18051806
SystemMessage = other.SystemMessage;
@@ -1958,6 +1959,11 @@ protected SessionConfig(SessionConfig? other)
19581959
/// </summary>
19591960
public IList<string>? SkillDirectories { get; set; }
19601961

1962+
/// <summary>
1963+
/// Additional directories to search for custom instruction files.
1964+
/// </summary>
1965+
public IList<string>? InstructionDirectories { get; set; }
1966+
19611967
/// <summary>
19621968
/// List of skill names to disable.
19631969
/// </summary>
@@ -2058,6 +2064,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
20582064
CreateSessionFsHandler = other.CreateSessionFsHandler;
20592065
GitHubToken = other.GitHubToken;
20602066
SkillDirectories = other.SkillDirectories is not null ? [.. other.SkillDirectories] : null;
2067+
InstructionDirectories = other.InstructionDirectories is not null ? [.. other.InstructionDirectories] : null;
20612068
Streaming = other.Streaming;
20622069
IncludeSubAgentStreamingEvents = other.IncludeSubAgentStreamingEvents;
20632070
SystemMessage = other.SystemMessage;
@@ -2235,6 +2242,11 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
22352242
/// </summary>
22362243
public IList<string>? SkillDirectories { get; set; }
22372244

2245+
/// <summary>
2246+
/// Additional directories to search for custom instruction files.
2247+
/// </summary>
2248+
public IList<string>? InstructionDirectories { get; set; }
2249+
22382250
/// <summary>
22392251
/// List of skill names to disable.
22402252
/// </summary>

dotnet/test/E2E/SessionConfigE2ETests.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,65 @@ public async Task Should_Apply_SystemMessage_On_Session_Resume()
249249
await session2.DisposeAsync();
250250
}
251251

252+
[Fact]
253+
public async Task Should_Apply_InstructionDirectories_On_Create()
254+
{
255+
var projectDir = Path.Join(Ctx.WorkDir, "instruction-create-project");
256+
var instructionDir = Path.Join(Ctx.WorkDir, "extra-create-instructions");
257+
var instructionFilesDir = Path.Join(instructionDir, ".github", "instructions");
258+
const string sentinel = "CS_CREATE_INSTRUCTION_DIRECTORIES_SENTINEL";
259+
Directory.CreateDirectory(projectDir);
260+
Directory.CreateDirectory(instructionFilesDir);
261+
await File.WriteAllTextAsync(
262+
Path.Join(instructionFilesDir, "extra.instructions.md"),
263+
$"Always include {sentinel}.");
264+
265+
var session = await CreateSessionAsync(new SessionConfig
266+
{
267+
WorkingDirectory = projectDir,
268+
InstructionDirectories = [instructionDir],
269+
});
270+
271+
await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" });
272+
273+
var exchange = Assert.Single(await Ctx.GetExchangesAsync());
274+
Assert.Contains(sentinel, GetSystemMessage(exchange));
275+
276+
await session.DisposeAsync();
277+
}
278+
279+
[Fact]
280+
public async Task Should_Apply_InstructionDirectories_On_Resume()
281+
{
282+
var projectDir = Path.Join(Ctx.WorkDir, "instruction-resume-project");
283+
var instructionDir = Path.Join(Ctx.WorkDir, "extra-resume-instructions");
284+
var instructionFilesDir = Path.Join(instructionDir, ".github", "instructions");
285+
const string sentinel = "CS_RESUME_INSTRUCTION_DIRECTORIES_SENTINEL";
286+
Directory.CreateDirectory(projectDir);
287+
Directory.CreateDirectory(instructionFilesDir);
288+
await File.WriteAllTextAsync(
289+
Path.Join(instructionFilesDir, "extra.instructions.md"),
290+
$"Always include {sentinel}.");
291+
292+
var session1 = await CreateSessionAsync(new SessionConfig
293+
{
294+
WorkingDirectory = projectDir,
295+
});
296+
var session2 = await ResumeSessionAsync(session1.SessionId, new ResumeSessionConfig
297+
{
298+
WorkingDirectory = projectDir,
299+
InstructionDirectories = [instructionDir],
300+
});
301+
302+
await session2.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" });
303+
304+
var exchange = Assert.Single(await Ctx.GetExchangesAsync());
305+
Assert.Contains(sentinel, GetSystemMessage(exchange));
306+
307+
await session2.DisposeAsync();
308+
await session1.DisposeAsync();
309+
}
310+
252311
[Fact]
253312
public async Task Should_Apply_AvailableTools_On_Session_Resume()
254313
{

dotnet/test/Unit/CloneTests.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
9494
Agent = "agent1",
9595
DefaultAgent = new DefaultAgentConfig { ExcludedTools = ["hidden-tool"] },
9696
SkillDirectories = ["/skills"],
97+
InstructionDirectories = ["/instructions"],
9798
DisabledSkills = ["skill1"],
9899
};
99100

@@ -114,6 +115,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
114115
Assert.Equal(original.Agent, clone.Agent);
115116
Assert.Equal(original.DefaultAgent!.ExcludedTools, clone.DefaultAgent!.ExcludedTools);
116117
Assert.Equal(original.SkillDirectories, clone.SkillDirectories);
118+
Assert.Equal(original.InstructionDirectories, clone.InstructionDirectories);
117119
Assert.Equal(original.DisabledSkills, clone.DisabledSkills);
118120
}
119121

@@ -127,6 +129,7 @@ public void SessionConfig_Clone_CollectionsAreIndependent()
127129
McpServers = new Dictionary<string, McpServerConfig> { ["s1"] = new McpStdioServerConfig { Command = "echo" } },
128130
CustomAgents = [new CustomAgentConfig { Name = "a1" }],
129131
SkillDirectories = ["/skills"],
132+
InstructionDirectories = ["/instructions"],
130133
DisabledSkills = ["skill1"],
131134
};
132135

@@ -138,6 +141,7 @@ public void SessionConfig_Clone_CollectionsAreIndependent()
138141
clone.McpServers!["s2"] = new McpStdioServerConfig { Command = "echo" };
139142
clone.CustomAgents!.Add(new CustomAgentConfig { Name = "a2" });
140143
clone.SkillDirectories!.Add("/more");
144+
clone.InstructionDirectories!.Add("/more-instructions");
141145
clone.DisabledSkills!.Add("skill99");
142146

143147
// Original is unaffected
@@ -146,6 +150,7 @@ public void SessionConfig_Clone_CollectionsAreIndependent()
146150
Assert.Single(original.McpServers!);
147151
Assert.Single(original.CustomAgents!);
148152
Assert.Single(original.SkillDirectories!);
153+
Assert.Single(original.InstructionDirectories!);
149154
Assert.Single(original.DisabledSkills!);
150155
}
151156

@@ -170,6 +175,7 @@ public void ResumeSessionConfig_Clone_CollectionsAreIndependent()
170175
McpServers = new Dictionary<string, McpServerConfig> { ["s1"] = new McpStdioServerConfig { Command = "echo" } },
171176
CustomAgents = [new CustomAgentConfig { Name = "a1" }],
172177
SkillDirectories = ["/skills"],
178+
InstructionDirectories = ["/instructions"],
173179
DisabledSkills = ["skill1"],
174180
};
175181

@@ -181,6 +187,7 @@ public void ResumeSessionConfig_Clone_CollectionsAreIndependent()
181187
clone.McpServers!["s2"] = new McpStdioServerConfig { Command = "echo" };
182188
clone.CustomAgents!.Add(new CustomAgentConfig { Name = "a2" });
183189
clone.SkillDirectories!.Add("/more");
190+
clone.InstructionDirectories!.Add("/more-instructions");
184191
clone.DisabledSkills!.Add("skill99");
185192

186193
// Original is unaffected
@@ -189,6 +196,7 @@ public void ResumeSessionConfig_Clone_CollectionsAreIndependent()
189196
Assert.Single(original.McpServers!);
190197
Assert.Single(original.CustomAgents!);
191198
Assert.Single(original.SkillDirectories!);
199+
Assert.Single(original.InstructionDirectories!);
192200
Assert.Single(original.DisabledSkills!);
193201
}
194202

@@ -247,6 +255,7 @@ public void Clone_WithNullCollections_ReturnsNullCollections()
247255
Assert.Null(clone.McpServers);
248256
Assert.Null(clone.CustomAgents);
249257
Assert.Null(clone.SkillDirectories);
258+
Assert.Null(clone.InstructionDirectories);
250259
Assert.Null(clone.DisabledSkills);
251260
Assert.Null(clone.Tools);
252261
Assert.Null(clone.DefaultAgent);

dotnet/test/Unit/SerializationTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,39 @@ public void SendMessageRequest_CanSerializeRequestHeaders_WithSdkOptions()
8181
Assert.Equal("trace-value", root.GetProperty("requestHeaders").GetProperty("X-Trace").GetString());
8282
}
8383

84+
[Fact]
85+
public void CreateSessionRequest_CanSerializeInstructionDirectories_WithSdkOptions()
86+
{
87+
var options = GetSerializerOptions();
88+
var requestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest");
89+
var request = CreateInternalRequest(
90+
requestType,
91+
("SessionId", "session-id"),
92+
("InstructionDirectories", new List<string> { "C:\\extra-instructions", "C:\\more-instructions" }));
93+
94+
var json = JsonSerializer.Serialize(request, requestType, options);
95+
using var document = JsonDocument.Parse(json);
96+
var root = document.RootElement;
97+
Assert.Equal("C:\\extra-instructions", root.GetProperty("instructionDirectories")[0].GetString());
98+
Assert.Equal("C:\\more-instructions", root.GetProperty("instructionDirectories")[1].GetString());
99+
}
100+
101+
[Fact]
102+
public void ResumeSessionRequest_CanSerializeInstructionDirectories_WithSdkOptions()
103+
{
104+
var options = GetSerializerOptions();
105+
var requestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest");
106+
var request = CreateInternalRequest(
107+
requestType,
108+
("SessionId", "session-id"),
109+
("InstructionDirectories", new List<string> { "C:\\resume-instructions" }));
110+
111+
var json = JsonSerializer.Serialize(request, requestType, options);
112+
using var document = JsonDocument.Parse(json);
113+
var root = document.RootElement;
114+
Assert.Equal("C:\\resume-instructions", root.GetProperty("instructionDirectories")[0].GetString());
115+
}
116+
84117
[Fact]
85118
public void McpHttpServerConfig_CanSerializeOauthOptions_WithSdkOptions()
86119
{

go/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
600600
req.DefaultAgent = config.DefaultAgent
601601
req.Agent = config.Agent
602602
req.SkillDirectories = config.SkillDirectories
603+
req.InstructionDirectories = config.InstructionDirectories
603604
req.DisabledSkills = config.DisabledSkills
604605
req.InfiniteSessions = config.InfiniteSessions
605606
req.GitHubToken = config.GitHubToken
@@ -789,6 +790,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
789790
req.DefaultAgent = config.DefaultAgent
790791
req.Agent = config.Agent
791792
req.SkillDirectories = config.SkillDirectories
793+
req.InstructionDirectories = config.InstructionDirectories
792794
req.DisabledSkills = config.DisabledSkills
793795
req.InfiniteSessions = config.InfiniteSessions
794796
req.GitHubToken = config.GitHubToken

go/client_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,65 @@ func TestResumeSessionRequest_Agent(t *testing.T) {
532532
})
533533
}
534534

535+
func TestCreateSessionRequest_InstructionDirectories(t *testing.T) {
536+
t.Run("includes instructionDirectories in JSON when set", func(t *testing.T) {
537+
req := createSessionRequest{InstructionDirectories: []string{`C:\extra-instructions`, `C:\more-instructions`}}
538+
data, err := json.Marshal(req)
539+
if err != nil {
540+
t.Fatalf("Failed to marshal: %v", err)
541+
}
542+
var m map[string]any
543+
if err := json.Unmarshal(data, &m); err != nil {
544+
t.Fatalf("Failed to unmarshal: %v", err)
545+
}
546+
got := m["instructionDirectories"].([]any)
547+
if len(got) != 2 || got[0] != `C:\extra-instructions` || got[1] != `C:\more-instructions` {
548+
t.Errorf("Expected instructionDirectories to be serialized, got %v", got)
549+
}
550+
})
551+
552+
t.Run("omits instructionDirectories from JSON when empty", func(t *testing.T) {
553+
req := createSessionRequest{}
554+
data, _ := json.Marshal(req)
555+
var m map[string]any
556+
json.Unmarshal(data, &m)
557+
if _, ok := m["instructionDirectories"]; ok {
558+
t.Error("Expected instructionDirectories to be omitted when empty")
559+
}
560+
})
561+
}
562+
563+
func TestResumeSessionRequest_InstructionDirectories(t *testing.T) {
564+
t.Run("includes instructionDirectories in JSON when set", func(t *testing.T) {
565+
req := resumeSessionRequest{
566+
SessionID: "s1",
567+
InstructionDirectories: []string{`C:\resume-instructions`},
568+
}
569+
data, err := json.Marshal(req)
570+
if err != nil {
571+
t.Fatalf("Failed to marshal: %v", err)
572+
}
573+
var m map[string]any
574+
if err := json.Unmarshal(data, &m); err != nil {
575+
t.Fatalf("Failed to unmarshal: %v", err)
576+
}
577+
got := m["instructionDirectories"].([]any)
578+
if len(got) != 1 || got[0] != `C:\resume-instructions` {
579+
t.Errorf("Expected instructionDirectories to be serialized, got %v", got)
580+
}
581+
})
582+
583+
t.Run("omits instructionDirectories from JSON when empty", func(t *testing.T) {
584+
req := resumeSessionRequest{SessionID: "s1"}
585+
data, _ := json.Marshal(req)
586+
var m map[string]any
587+
json.Unmarshal(data, &m)
588+
if _, ok := m["instructionDirectories"]; ok {
589+
t.Error("Expected instructionDirectories to be omitted when empty")
590+
}
591+
})
592+
}
593+
535594
func TestOverridesBuiltInTool(t *testing.T) {
536595
t.Run("OverridesBuiltInTool is serialized in tool definition", func(t *testing.T) {
537596
tool := Tool{

0 commit comments

Comments
 (0)